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. 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 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
- 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. - 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. - The same call tells you when a server is older. It answers
server/discoverwith-32601(Method not found), and the client falls back to the legacy handshake. Software development kits (SDKs) do this for you. - Identity travels on every message. Clients SHOULD send
clientInfoin each request’s_meta, and servers SHOULD sendserverInfoin each result’s_meta. - 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. 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 | Its v2 MCP runtime asks HTTP and stdio servers whether they support the newer revision, and uses it with those that do |
| Ruby SDK | 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 | Can be pinned to 2026-07-28, and then rejects initialize handshakes |
Python SDK, 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:
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:
MCPServerdeclarestools,resourcesandpromptswhatever you register: a server with nothing registered declares the same three.- Without a
CacheHint, its discovery result carriesttlMs: 0andcacheScope: "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/clientInfoin_meta. - Servers SHOULD identify themselves in each result’s
_metawithio.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, 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 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
mcp2.3.0 on 8 October 2026 with the in-memoryClient, and the output above is what it printed. The notes onMCPServer’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.
SeriesWhat an MCP server actually does in 2026Part 3 of 35
- What an MCP server does in 2026: much more than a list of tools
- MCP goes stateless: what changed in the 2026-07-28 specification
- server/discover: the one method every MCP server must implement
- Server instructions: the paragraph every model reads first
- Stateless MCP requests: _meta, resultType and explicit handles
- Caching hints and pagination in MCP
- MCP transports in 2026: stdio and Streamable HTTP