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/list with version 2099-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-28 and 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:

  1. What server/discover asks and answers.
  2. How every request carries its own version and capabilities.
  3. What happens when versions don't match.
  4. How capability flags tell each side what it may use.
  5. How a client hears that a list changed, with subscriptions/listen.
  6. The old initialize handshake, 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:

FieldMeaning
supportedVersionsThe spec versions the server speaks
capabilitiesWhat the server offers: tools, resources, prompts, completions
instructionsOptional notes for the host on how to use this server
_meta → serverInfoThe server's name and version
ttlMs, cacheScopeHow 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-Version header.

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 with completion/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:

  1. First, notifications/subscriptions/acknowledged: "I am listening for you."
  2. Later, whenever a list changes, a notice such as notifications/tools/list_changed.
  3. The client then calls tools/list again 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/listen stream. 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 capabilities has no prompts, don't call prompts/list.
  • Comparing versions as numbers. Versions are date text like 2026-07-28. They sort correctly as text.
  • Calling initialize on a 2026-07-28 server. Use server/discover. Fall back to initialize only for older servers.
  • Re-listing on a timer instead of listening. Polling wastes requests. Use subscriptions/listen when the server supports listChanged.
  • 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/discover returns supportedVersions, capabilities, instructions, and the server's info.
  • Every request carries its version in _meta (and over HTTP, in MCP-Protocol-Version).
  • A wrong version gets -32022 with the supported list in data. Pick the newest shared version.
  • Use only features listed in capabilities. The client sends its own capabilities on every request.
  • subscriptions/listen opens a stream for change notices. After list_changed, fetch the list again.
  • Older servers use initialize + notifications/initialized and sessions. Many clients fall back to it.

Next: Chapter 3, Tools, how to design tools a model can use well.