Discovery and Versioning
By the Syncfusion DevTutorials team · Last updated
server/discover, per-request versions in _meta, the -32022 unsupported version error, server and client capabilities, list changes through subscriptions/listen, the legacy initialize handshake, and clean shutdown.
When you arrive at a new hotel, you go to the front desk first. You ask: "Do you speak English? Is there a gym? Is breakfast included?" Now you know what the hotel offers, and you don't ask the pool staff for a taxi.
An MCP client does the same with a new server. Before it uses anything, it asks: "Which versions of MCP do you speak? What do you offer?" This is called discovery.
The main idea: a client calls server/discover to learn the server's versions and capabilities. After that, every request says which version it uses, so client and server always know they understand each other.
A Real-World Example
Brightgrid upgrades its help-desk server to the 2026-07-28 spec. Ravi's old test script still asks for a version from 2099 by mistake.
- The script sends
tools/listwith version2099-01-01. - The server answers with error
-32022, "Unsupported protocol version", and lists the versions it does support:2026-07-28. - The script switches to
2026-07-28and tries again. It works.
Later, the help-desk team adds a new tool. The support app is already connected. The server sends a short notice, "my tool list changed", and the app asks for the list again. The new tool shows up without a restart.
In simple words: discovery is the front-desk question. Versions travel with every request. Change notices keep lists fresh.
What You Will Learn
In this lesson, you will learn:
- What
server/discoverasks and answers. - How every request carries its own version and capabilities.
- What happens when versions don't match.
- How capability flags tell each side what it may use.
- How a client hears that a list changed, with
subscriptions/listen. - The old
initializehandshake, and how to shut down cleanly.
In the MCP Lab, open Server code and look for the Update live button. It changes the server without reconnecting and sends a "list changed" notice. The Inspector shows the whole exchange.
Server Discovery
The client discovers the server, opens a listen stream for change notices, and sends its version with every request. When a list changes, it lists again.
The server/discover Request
server/discover is a normal request. Like every request, it carries _meta with the client's version, name, and capabilities. The answer tells the client:
| Field | Meaning |
|---|---|
supportedVersions | The spec versions the server speaks |
capabilities | What the server offers: tools, resources, prompts, completions |
instructions | Optional notes for the host on how to use this server |
_meta → serverInfo | The server's name and version |
ttlMs, cacheScope | How long the client may keep this answer |
Press the button, then click the server/discover row in the Inspector. Compare it with this code:
Read a server/discover result
// The real server/discover result from the lab's help-desk server (trimmed).
const discover = {
supportedVersions: ["2026-07-28"],
capabilities: {
tools: { listChanged: true },
resources: { listChanged: true },
prompts: { listChanged: true },
completions: {},
},
instructions: "Brightgrid help desk tools. Read-only lookups are safe to call.",
resultType: "complete",
ttlMs: 0,
cacheScope: "private",
_meta: {
"io.modelcontextprotocol/serverInfo": {
name: "brightgrid-helpdesk",
version: "1.0.0",
},
},
};
const info = discover._meta["io.modelcontextprotocol/serverInfo"];
console.log("server: ", info.name, info.version);
console.log("versions: ", discover.supportedVersions.join(", "));
console.log("offers: ", Object.keys(discover.capabilities).join(", "));
console.log("instructions:", discover.instructions);
Expected output:
server: brightgrid-helpdesk 1.0.0
versions: 2026-07-28
offers: tools, resources, prompts, completions
instructions: Brightgrid help desk tools. Read-only lookups are safe to call.
When to Call It
A client usually calls server/discover once when it first connects. It can call it again at any time, for example to check that a server is healthy, or after the server was updated. Because servers are stateless, server/discover does not open anything. It just answers a question.
Hosts can show the server's instructions to the model as part of the context. Good instructions say what the server is for and when to use its tools, in one or two sentences. Long instructions cost tokens on every request.
Now that the client knows what the server offers, it also needs to agree on which protocol version to use.
Versioning
Per-Request Version
In 2026-07-28, the version is not agreed once and remembered. Every request says its version, twice:
- In the body, in
_meta→io.modelcontextprotocol/protocolVersion. - Over HTTP, also in the
MCP-Protocol-Versionheader.
The server checks the version on every request. This is what allows any copy of a server to answer any request.
Choosing a Version
Versions are dates, so the newest one is simply the latest date. A client picks the newest version that both sides support:
Pick the newest shared version
// Pick the newest version both sides support. Dates sort as text.
function pickVersion(clientVersions, serverVersions) {
const shared = clientVersions.filter((v) => serverVersions.includes(v)).sort();
return shared.length ? shared[shared.length - 1] : null;
}
const client = ["2025-11-25", "2026-07-28"];
console.log(pickVersion(client, ["2026-07-28"]));
console.log(pickVersion(client, ["2025-06-18", "2025-11-25"]));
console.log(pickVersion(client, ["2024-11-05"]));
Expected output:
2026-07-28
2025-11-25
null
UnsupportedProtocolVersion
If a request names a version the server does not speak, the server refuses it with error -32022. The error's data lists the versions it does support and the version that was asked for. The client can then pick a shared version and try again, or tell the user that the server and the app do not match.
Recover from a version error
// The server refused our version. Its error says which versions it does support.
const errorReply = {
jsonrpc: "2.0",
id: 5,
error: {
code: -32022,
message: "Unsupported protocol version: 2099-01-01",
data: { supported: ["2026-07-28"], requested: "2099-01-01" },
},
};
const mine = ["2025-11-25", "2026-07-28", "2099-01-01"];
if (errorReply.error.code === -32022) {
const usable = mine.filter((v) => errorReply.error.data.supported.includes(v));
console.log("requested:", errorReply.error.data.requested);
console.log(
"retry with:",
usable.length
? usable[usable.length - 1]
: "none: tell the user the server is too old or too new",
);
}
Expected output:
requested: 2099-01-01
retry with: 2026-07-28
Capability Flags
Server Capabilities
The capabilities object says what the server offers. Each key is a feature. Some keys hold extra flags:
tools: { listChanged: true }: the server has tools, and it can say when the list changes.resources,prompts: the same idea for resources and prompts.completions: {}: the server can suggest argument values withcompletion/complete.
A client must not use a feature the server did not announce. If there is no resources key, do not send resources/list.
Check capabilities before you ask
// Only use a feature the server announced in its capabilities.
const caps = {
tools: { listChanged: true },
prompts: { listChanged: true },
completions: {},
};
function can(feature) {
switch (feature) {
case "list tools":
return "tools" in caps;
case "read resources":
return "resources" in caps;
case "autocomplete":
return "completions" in caps;
case "hear tool list changes":
return caps.tools?.listChanged === true;
default:
return false;
}
}
for (const f of [
"list tools",
"read resources",
"autocomplete",
"hear tool list changes",
]) {
console.log(f.padEnd(23), can(f) ? "yes" : "no: don't ask");
}
Expected output:
list tools yes
read resources no: don't ask
autocomplete yes
hear tool list changes yes
Client Capabilities
The client tells the server what it can do in _meta → clientCapabilities on every request. For example, elicitation: { form: {} } means "my host can show a form to the user". The MCP Lab's client says this, which is why the cancel_order tool may ask you to confirm. A server must not ask for input the client cannot collect.
Extensions
Some features are not part of the core spec but are agreed as optional extensions, like the Tasks extension for very long jobs. Servers and clients announce the extensions they support, just like other capabilities. You will meet extensions in Chapter 7.
List Changes
subscriptions/listen
Tool, resource, and prompt lists can change while a client is connected. A client that wants to hear about it sends a subscriptions/listen request. It says which notices it wants, for example toolsListChanged: true.
The server answers with a stream that stays open:
- First,
notifications/subscriptions/acknowledged: "I am listening for you." - Later, whenever a list changes, a notice such as
notifications/tools/list_changed. - The client then calls
tools/listagain to get the new list.
The notice only says "something changed." The client fetches the new list itself.
Try it: in Server code, copy the lookup_order block, rename the copy to track_parcel, and press Update live (send list_changed). Then open the Inspector. You will see the three change notices arrive on the listen stream, and then new tools/list, resources/list, and prompts/list requests.
A changed tool list can be a security risk. A server could swap a harmless tool for a harmful one with the same name after the user approved it. Hosts should show the user when tools change and re-check approvals for changed tools.
The Old Handshake and Shutting Down
Legacy initialize
Servers on 2025-11-25 and earlier start differently. The client first sends initialize with its version and capabilities. The server answers with its own, and over HTTP it creates a session with an Mcp-Session-Id. The client then sends notifications/initialized. Only then can real requests begin, and every one must carry the session id.
Old handshake vs new discovery
// Starting up: the old handshake (2025-11-25) next to the new discovery (2026-07-28).
const old = [
"client -> initialize { protocolVersion, capabilities, clientInfo }",
"server -> result { protocolVersion, capabilities, serverInfo } (+ Mcp-Session-Id over HTTP)",
"client -> notifications/initialized",
"client -> tools/list (with the session id over HTTP)",
];
const now = [
"client -> server/discover { _meta: version, clientInfo, capabilities }",
"server -> result { supportedVersions, capabilities, instructions }",
"client -> tools/list { _meta: version, clientInfo, capabilities }",
];
console.log("2025-11-25:");
old.forEach((s, i) => console.log(` ${i + 1}. ${s}`));
console.log("2026-07-28:");
now.forEach((s, i) => console.log(` ${i + 1}. ${s}`));
console.log(
"messages before the first real request:",
old.length - 1,
"vs",
now.length - 1,
);
Expected output:
2025-11-25:
1. client -> initialize { protocolVersion, capabilities, clientInfo }
2. server -> result { protocolVersion, capabilities, serverInfo } (+ Mcp-Session-Id over HTTP)
3. client -> notifications/initialized
4. client -> tools/list (with the session id over HTTP)
2026-07-28:
1. client -> server/discover { _meta: version, clientInfo, capabilities }
2. server -> result { supportedVersions, capabilities, instructions }
3. client -> tools/list { _meta: version, clientInfo, capabilities }
messages before the first real request: 3 vs 2
Many clients today speak both: they try server/discover first and fall back to initialize if the server is older. The official TypeScript SDK can do this automatically.
Shutting Down Cleanly
With no sessions, there is little to shut down:
- Streamable HTTP: the client stops sending requests and closes any open
subscriptions/listenstream. There is no "goodbye" message. - stdio: the host closes the server's stdin, waits for the process to exit, and only forces it to stop if it hangs.
A server should finish or cancel work in progress, flush its logs, and release files and database connections when it stops.
Common mistakes
- Using a feature that wasn't announced. If
capabilitieshas noprompts, don't callprompts/list. - Comparing versions as numbers. Versions are date text like
2026-07-28. They sort correctly as text. - Calling
initializeon a 2026-07-28 server. Useserver/discover. Fall back toinitializeonly for older servers. - Re-listing on a timer instead of listening. Polling wastes requests. Use
subscriptions/listenwhen the server supportslistChanged. - Not re-checking changed tools. A tool with the same name can do something new after a change. Treat changes as new tools.
Exercises
Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.
What to Remember
server/discoverreturnssupportedVersions,capabilities,instructions, and the server's info.- Every request carries its version in
_meta(and over HTTP, inMCP-Protocol-Version). - A wrong version gets
-32022with the supported list indata. Pick the newest shared version. - Use only features listed in capabilities. The client sends its own capabilities on every request.
subscriptions/listenopens a stream for change notices. Afterlist_changed, fetch the list again.- Older servers use
initialize+notifications/initializedand sessions. Many clients fall back to it.
Next: Chapter 3, Tools, how to design tools a model can use well.