cd /news/agent-protocols/server-discover-the-one-method-every… · home › topics › agent-protocols › article
[ARTICLE · art-148817] src=pournasserian.com ↗ pub= topic=agent-protocols verified=true sentiment=· neutral

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

The 2026-07-28 revision of the Model Context Protocol (MCP) requires every server to implement the server/discover method, which replaces the removed initialize handshake by advertising supported protocol versions, capabilities and identity without any prior handshake. Clients may call server/discover first or negotiate inline by sending their protocol version in each request's _meta, receiving error -32022 for a version the server cannot serve, while older servers answer with -32601 (Method not found) and clients fall back to the legacy handshake. Identity now travels on every message, with clients SHOULD sending clientInfo in each request's _meta and servers SHOULD sending serverInfo in each result's _meta.

by read9 min views1 publishedOct 9, 2026
server/discover: the one method every MCP server must implement
Image: source

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 answersserver/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 sendclientInfo in each request’s_meta , and servers SHOULD sendserverInfo 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. 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 probesserver/discover and falls back toinitialize in its defaultmode="auto" .mode="legacy" uses the handshake only, andmode="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 declarestools ,resources andprompts whatever you register: a server with nothing registered declares the same three.
  • Without a CacheHint , its discovery result carriesttlMs: 0 andcacheScope: "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 withio.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 mcp 2.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.

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
  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
── more in #agent-protocols 4 stories · sorted by recency
── more on @model context protocol 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/server-discover-the-…] indexed:0 read:9min 2026-10-09 · —