# Discovery and Versioning

Course: [Model Context Protocol](https://www.devtutorials.ai/learn/mcp) · Section: Protocol Architecture
Source: https://www.devtutorials.ai/learn/mcp/architecture/discovery-and-versioning

> 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 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:

```js runnable title="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:

```text
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 <code>instructions</code> 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:

```js runnable title="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:

```text
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.

```js runnable title="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:

```text
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`.

```js runnable title="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:

```text
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.

```js runnable title="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:

```text
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.
