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

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

  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"
  }
}
FieldWhat it’s for
supportedVersionsThe protocol revisions this server can speak, on a per-request basis
capabilitiesWhich primitives and utilities exist (tools, resources, prompts and so on) and which extensions the server supports
instructionsOptional 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, cacheScopeHow 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:

ServerClientalt[Server speaks 2026-07-28][Older server (pre-2026-07-28)]server/discoversupportedVersions [2026-07-28], capabilities, instructionstools/list + _meta protocolVersion 2026-07-28tools + ttlMs + cacheScopeerror -32601 Method not foundinitialize (legacy handshake)InitializeResultnotifications/initializedtools/call + _meta protocolVersion 2099-01-01error -32022 UnsupportedProtocolVersion

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

Host or SDKWhat it does
Claude CodeIts v2 MCP runtime asks HTTP and stdio servers whether they support the newer revision, and uses it with those that do
Ruby SDKProbes 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# SDKCan be pinned to 2026-07-28, and then rejects initialize handshakes
Python SDK, mcp 2.3.0Client 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:

  • 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, and a few more:

CapabilityDeclare it whenNotes
toolsYou expose toolslistChanged: true if the set can change at runtime
resourcesYou expose resources, embed resources, or serve SkillsThe Skills extension requires it
promptsYou expose promptslistChanged as for tools
completionsYou support argument completion for prompts or resource templates
extensionsYou support any extensionA 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

CodeNameWhen
-32601Method not foundAn unknown method, or a legacy method such as tasks/result
-32602Invalid paramsBad arguments, an unknown tool or prompt, or a resource not found (which used to be -32002)
-32603Internal errorAn unexpected server failure
-32020HeaderMismatchThe HTTP routing headers disagree with the body
-32021MissingRequiredClientCapabilityThe operation needs a capability or extension the client didn’t declare
-32022UnsupportedProtocolVersionThe 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:

RangeWhat the specification says
-32020 to -32099Reserved for the MCP specification. Implementations MUST NOT emit codes from it that the specification doesn’t define
-32000 to -32019A 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 ownSHOULD 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.
SeriesWhat an MCP server actually does in 2026Part 3 of 35
  1. What an MCP server does in 2026: much more than a list of tools
  2. MCP goes stateless: what changed in the 2026-07-28 specification
  3. server/discover: the one method every MCP server must implement
  4. Server instructions: the paragraph every model reads first
  5. Stateless MCP requests: _meta, resultType and explicit handles
  6. Caching hints and pagination in MCP
  7. MCP transports in 2026: stdio and Streamable HTTP

I’m Amir Pournasserian. I build AI and platform systems for a living, maintain FluentCMS and YeSvelte, and write here about what I find along the way.