MCP 2026-07-28 one lesson per page
23 lessons
New, mandatoryArchitecture & Components · 14%Track 2 · 3 / 14

server/discover: ask what a server can do

With no handshake, the server needs somewhere else to announce itself. Every server MUST now implement server/discover.

Listen to this lessonAudio overview in Gemini Notebook · about 15–25 min · opens in a new tab

Cataloguing 500 internal servers

Your platform team runs an internal "MCP marketplace" that lists every server the company runs: 500 of them, with their tools and whether they support Tasks.

In the legacy world, the nightly crawler had to initialize each server, call three different list methods, and tear the session down. Some servers rate-limited it; others kept zombie sessions around.

Now it's one server/discover call per server. The answer is marked cacheScope: "public" with a one-hour TTL, so the company gateway serves repeat lookups from cache. The marketplace shows a green "2026-07-28" badge for servers whose supportedVersions include it, and flags the ones that still answer with a legacy error.

server/discover takes no parameters beyond the standard _meta. It returns supportedVersions, capabilities (including any extensions), optional instructions for the model, and serverInfo in _meta. The result is cacheable, so it also carries ttlMs and cacheScope.

Servers MUST implement it. Calling it is optional for clients: a client may call any method directly and handle -32022 if the version is wrong.

Its second job is as a probe on stdio. A dual-era client SHOULD send server/discover first. A result, or a recognised modern error, means a modern server. Any other error, or a timeout, means a legacy server: fall back to initialize. Cache that answer for the lifetime of the server process.

Everything InitializeResult used to carry needed a new home. Putting it in a cacheable method means one call can show a server's identity and abilities, without probing tools/list, prompts/list and resources/list separately, and without opening anything that looks like a session.

Message flow in 2026-07-28
Dual-era clientServer (stdio)server/discoverDiscoverResult → modern, stay modernor: unknown method / non-modern error / timeoutinitialize (legacy fallback)InitializeResult
Solid arrows are requests, dashed are responses or notifications. Red ✕ is gone; green is new.

Where server identity lives now

2026-07-28
{"jsonrpc": "2.0", "id": "discover-1", "method": "server/discover", "params": { "_meta": {   "io.modelcontextprotocol/protocolVersion": "2026-07-28",   "io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },   "io.modelcontextprotocol/clientCapabilities": {} }}}{  "jsonrpc": "2.0",  "id": "discover-1",  "result": {    "resultType": "complete",    "supportedVersions": ["2026-07-28"],    "capabilities": { "tools": {}, "resources": {} },    "_meta": {      "io.modelcontextprotocol/serverInfo": { "name": "ExampleServer", "version": "1.0.0" }    },    "instructions": "This server provides weather and resource utilities.",    "ttlMs": 3600000,    "cacheScope": "public"  }}
What it used to look like (legacy, for comparison only)
Legacybefore
// legacy: identity and abilities came back from initialize{"jsonrpc": "2.0", "id": 1, "result": {  "protocolVersion": "2025-11-25",  "capabilities": { "tools": {}, "resources": {} },  "serverInfo": { "name": "ExampleServer", "version": "1.0.0" },  "instructions": "This server provides weather and resource utilities."}}

Verbatim from the spec. An extension such as Tasks is advertised here, under capabilities.extensions.

  • It's a new handler that every server must have. Without it, a dual-era stdio client will decide your modern server is legacy.
  • Move everything your initialize handler returned into it, and add supportedVersions, ttlMs and cacheScope.
  • If you support extensions (Tasks, Apps and so on), this is where you advertise them.
First time here? Set up the test helper (once per terminal)
ShellSetup
# 1. In a SECOND terminal, start the reference server (Node 18+, no dependencies)curl -sO https://www.diegozuluaga.dev/mcpa/reference-server.mjsnode reference-server.mjs                 # http://localhost:3000/mcp, logs appear here # 2. In THIS terminal, define the helper every test uses (bash or zsh)export MCP=http://localhost:3000/mcpMETA='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}'mcp() {  # usage: mcp <method> '<json body>' [extra curl args...]  curl -sS -N "$MCP" \    -H 'Content-Type: application/json' \    -H 'Accept: application/json, text/event-stream' \    -H 'MCP-Protocol-Version: 2026-07-28' \    -H "Authorization: Bearer ${MCP_USER:-alice}" \    -H "Mcp-Method: $1" "${@:3}" -d "$2" \    -w '\nHTTP %{http_code}\n'}# Demo auth: the reference server treats the bearer token as the user's name.# Prefix a command with MCP_USER=bob to act as someone else.
ShellTests
# HTTPmcp server/discover '{"jsonrpc":"2.0","id":"d1","method":"server/discover","params":{'"$META"'}}'# expect: supportedVersions, capabilities (with extensions), serverInfo in _meta, ttlMs and cacheScope # stdio: the same server, speaking newline-delimited JSON on stdin/stdout{ printf '%s\n' '{"jsonrpc":"2.0","id":"d1","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'; sleep 1; } | node reference-server.mjs --stdio# the sleep keeps stdin open long enough for the reply# a legacy server would answer "method not found" (or nothing): that is the fallback signal

Answer all 2 correctly and this lesson is marked as learned.

Q1Must a client call server/discover before calling tools/call?

Q2A dual-era client sends server/discover over stdio and gets back "Method not found" from a server it has never seen. What should it do?

Feedback or a correction? Email diego [at] diegozuluaga [dot] dev or open an issue on GitHub.

Content CC BY 4.0 · Code MIT

Tip: ← and → move between lessons. Hover any heading and press # to copy a link to it.