# server/discover: the one method every MCP server must implement

> Source: <https://pournasserian.com/writing/mcp-2026-server-discover>
> Published: 2026-10-09 00:00:00+00:00

# server/discover: the one method every MCP server must implement

When the 2026-07-28 revision of the Model Context Protocol (MCP) dropped the `initialize` handshake, a client needed another way to learn what a server supports before it starts using it. That job now belongs to a single method, [`server/discover`](https://modelcontextprotocol.io/specification/2026-07-28/server/discover). It’s the one method every 2026-07-28 server MUST implement, which is why it gets a part of its own.

This is part 3 of my series on what an MCP server does under the 2026-07-28 specification. [Part 2](https://pournasserian.com/writing/mcp-2026-what-changed) covered what the revision took away, the handshake among it. This part covers what took its place: what discovery returns, how a client settles on a protocol version with old and new servers alike, and how identity now travels with every message. The facts are as I read them in October 2026.

## In brief

1. **Every 2026-07-28 server MUST implement `server/discover`.** It advertises the protocol versions the server supports, its capabilities and its identity, and a client can call it without any prior handshake.
2. **Calling it first is optional for the client.** Every request carries its protocol version in`_meta` , so negotiation can also happen inline, and a version the server can’t serve gets error`-32022` .
3. **The same call tells you when a server is older.** It answers`server/discover` with`-32601` (Method not found), and the client falls back to the legacy handshake. Software development kits (SDKs) do this for you.
4. **Identity travels on every message.** Clients SHOULD send`clientInfo` in each request’s`_meta` , and servers SHOULD send`serverInfo` in each result’s`_meta` .
5. **Capabilities describe the server, not the connection.** My advice is to keep discovery cheap, deterministic and cacheable.

## What discovery returns

A client that calls `server/discover` can use it to pick a version up front, or as a backward-compatibility probe on standard input and output (stdio). The request is a JSON-RPC 2.0 message, and it can be this small:

```
{ "jsonrpc": "2.0", "id": 1, "method": "server/discover" }
```

And here is a result, from a documentation server called `acme-docs`:

```
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "supportedVersions": ["2026-07-28"],
    "capabilities": {
      "tools": { "listChanged": true },
      "resources": {},
      "prompts": {},
      "extensions": {
        "io.modelcontextprotocol/ui": {},
        "io.modelcontextprotocol/tasks": {}
      }
    },
    "instructions": "Use search before fetch. Results expire after 24 hours.",
    "_meta": {
      "io.modelcontextprotocol/serverInfo": { "name": "acme-docs", "version": "3.2.0" }
    },
    "ttlMs": 3600000,
    "cacheScope": "public"
  }
}
```

| Field | What it’s for | 
|---|---|
| `supportedVersions` | The protocol revisions this server can speak, on a per-request basis | 
| `capabilities` | Which primitives and utilities exist ( `tools` ,`resources` ,`prompts` and so on) and which extensions the server supports | 
| `instructions` | Optional plain-text guidance for the model | 
| `_meta["io.modelcontextprotocol/serverInfo"]` | The server’s identity: a name and a version, and optionally a title and icons | 
| `ttlMs` ,`cacheScope` | How long, and how widely, the result may be cached | 

It helps to set it beside MCP Server Cards, which came up in [my survey of MCP servers for websites](https://pournasserian.com/writing/mcp-servers-for-websites-2026). A Server Card helps an agent find a server before it connects, and says how to connect. `server/discover` runs after the client has connected, and tells it what this server can do.

## Version negotiation, and the fallback to the handshake

Each request names the protocol version it uses. When a client asks for a version the server can’t serve, the server returns `UnsupportedProtocolVersionError`, code `-32022`. A server that predates 2026-07-28 doesn’t know `server/discover` at all, so it answers with error `-32601` (Method not found), and the client falls back to `initialize`:

You’ll rarely write this yourself. As of October 2026, these hosts and SDKs handle it for you:

| Host or SDK | What it does | 
|---|---|
| [Claude Code](https://code.claude.com/docs/en/mcp) | Its v2 MCP runtime asks HTTP and stdio servers whether they support the newer revision, and uses it with those that do | 
| [Ruby SDK](https://ruby.sdk.modelcontextprotocol.io/server/discovery/) | Probes `server/discover` and falls back to the handshake. Its docs note that “the lifecycle is a per-request property, not a server-wide mode” | 
| [C# SDK](https://csharp.sdk.modelcontextprotocol.io/v2/api/ModelContextProtocol.Server.McpServerOptions.html) | Can be pinned to 2026-07-28, and then rejects `initialize` handshakes | 
| [Python SDK](https://py.sdk.modelcontextprotocol.io/protocol-versions/) ,`mcp` 2.3.0 | `Client` probes`server/discover` and falls back to`initialize` in its default`mode="auto"` .`mode="legacy"` uses the handshake only, and`mode="2026-07-28"` (the only version string 2.3.0 accepts) pins that version without probing | 

### Reading discovery from Python

Here is a cut-down `acme-docs` server (one tool, no extensions) built with the official Python SDK’s `MCPServer`. The in-memory `Client` connects to it and prints what discovery returned:

``` python
import anyio
from mcp import Client
from mcp.server import CacheHint, MCPServer

mcp = MCPServer(
    "acme-docs",
    version="3.2.0",
    instructions="Use search before fetch. Results expire after 24 hours.",
    cache_hints={"server/discover": CacheHint(ttl_ms=3_600_000, scope="public")},
)

@mcp.tool()
def search(query: str) -> list[str]:
    """Search the docs and return matching page ids."""
    return []  # the search itself doesn't matter here

async def main():
    async with Client(mcp) as client:
        print(client.protocol_version)
        print(client.server_capabilities.model_dump(by_alias=True, exclude_none=True))
        print(client.server_info.name, client.server_info.version)
        found = client.session.discover_result
        print(found.supported_versions, found.ttl_ms, found.cache_scope)
        print(found.instructions)
    async with Client(mcp, mode="legacy") as old:
        print(old.protocol_version, old.session.discover_result)

anyio.run(main)
```

It prints:

```
2026-07-28
{'prompts': {'listChanged': True}, 'resources': {'subscribe': True, 'listChanged': True}, 'tools': {'listChanged': True}}
acme-docs 3.2.0
['2026-07-28'] 3600000 public
Use search before fetch. Results expire after 24 hours.
2025-11-25 None
```

The last line is the same server answering the legacy handshake: with `mode="legacy"` the client settles on 2025-11-25 and holds no discover result.

Three SDK defaults are worth knowing, because the sample above overrides two of them:

- `MCPServer` declares`tools` ,`resources` and`prompts` whatever you register: a server with nothing registered declares the same three.
- Without a `CacheHint` , its discovery result carries`ttlMs: 0` and`cacheScope: "private"` .
- Without `version=` , it reports an empty version.

## Identity and capabilities

### Identity on every message

Identity no longer travels only once, at connection time:

- Clients SHOULD identify themselves on each request with `io.modelcontextprotocol/clientInfo` in`_meta` .
- Servers SHOULD identify themselves in each result’s `_meta` with`io.modelcontextprotocol/serverInfo` .

The Python SDK does both for you: `Client` puts `clientInfo` into every request it makes under 2026-07-28, and `MCPServer` puts `serverInfo` into every result it returns under that revision.

The spec only asks clients to send `clientInfo`. My advice goes further: log it with every request. When something works in one host and fails in another, that field tells you who called, and with which version.

### What to declare

The capabilities map names the primitives you offer, the three I described in [part 1](https://pournasserian.com/writing/mcp-2026-what-a-server-does), and a few more:

| Capability | Declare it when | Notes | 
|---|---|---|
| `tools` | You expose tools | `listChanged: true` if the set can change at runtime | 
| `resources` | You expose resources, embed resources, or serve Skills | The Skills extension requires it | 
| `prompts` | You expose prompts | `listChanged` as for tools | 
| `completions` | You support argument completion for prompts or resource templates |  | 
| `extensions` | You support any extension | A map from extension identifier to a settings object; `{}` means no settings | 

With the Python SDK, the first three rows aren’t your choice: `MCPServer` declares all three, as the output above showed.

Capabilities describe what the server can do. They must not vary per connection or change as a side effect of other requests, though they may vary with the caller’s authorization, because credentials are input to each request. The specification sets the same rule for the tools, resources and prompts a server lists.

My advice: make `server/discover` cheap, deterministic and cacheable, and don’t hit a database to build it. Clients may call it often, and intermediaries may cache it publicly if you set `cacheScope: "public"`. If your capabilities depend on who is calling, either keep discovery generic and filter at the list level, or mark the result `private`.

## Error codes you’ll meet

| Code | Name | When | 
|---|---|---|
| `-32601` | Method not found | An unknown method, or a legacy method such as `tasks/result` | 
| `-32602` | Invalid params | Bad arguments, an unknown tool or prompt, or a resource not found (which used to be `-32002` ) | 
| `-32603` | Internal error | An unexpected server failure | 
| `-32020` | HeaderMismatch | The HTTP routing headers disagree with the body | 
| `-32021` | MissingRequiredClientCapability | The operation needs a capability or extension the client didn’t declare | 
| `-32022` | UnsupportedProtocolVersion | The requested protocol version isn’t supported | 

The ranges matter when you add codes of your own. Here is how the [specification’s error codes section](https://modelcontextprotocol.io/specification/2026-07-28/basic/index) divides them:

| Range | What the specification says | 
|---|---|
| `-32020` to`-32099` | Reserved for the MCP specification. Implementations MUST NOT emit codes from it that the specification doesn’t define | 
| `-32000` to`-32019` | A legacy range. Codes SDKs already use there are grandfathered, but new codes MUST NOT be allocated in it, and new implementations SHOULD NOT use it at all. Apart from `-32002` , receivers MUST NOT assume any specific meaning for its codes | 
| Codes of your own | SHOULD sit outside the JSON-RPC reserved range, `-32768` to`-32000` | 

## Method and caveats

- Built from my guide to what an MCP server does in 2026, written against the 2026-07-28 specification.
- What Claude Code, the Ruby SDK and the C# SDK do is as their documentation described it in October 2026.
- The Python sample was run against `mcp` 2.3.0 on 8 October 2026 with the in-memory`Client` , and the output above is what it printed. The notes on`MCPServer` ’s defaults come from a second run, with an empty server, and from the package’s source.
- Where the specification is more precise than my notes (the error-code ranges), this article follows the specification’s error codes section.

## Series What an MCP server actually does in 2026 Part 3 of 35

1. [What an MCP server does in 2026: much more than a list of tools](https://pournasserian.com/writing/mcp-2026-what-a-server-does)
2. [MCP goes stateless: what changed in the 2026-07-28 specification](https://pournasserian.com/writing/mcp-2026-what-changed)
3. server/discover: the one method every MCP server must implement
4. [Server instructions: the paragraph every model reads first](https://pournasserian.com/writing/mcp-2026-server-instructions)
5. [Stateless MCP requests: _meta, resultType and explicit handles](https://pournasserian.com/writing/mcp-2026-stateless-requests)
6. [Caching hints and pagination in MCP](https://pournasserian.com/writing/mcp-2026-caching-and-pagination)
7. [MCP transports in 2026: stdio and Streamable HTTP](https://pournasserian.com/writing/mcp-2026-transports)
