Discovery
server/discover replaces the initialize handshake.
Modern clients do not negotiate. Every request carries what the server needs to know, and a single cacheable server/discover call tells the client what the server is.
#Per-Request _meta
Instead of handshake state, each request carries reserved keys in params._meta:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "hello",
"arguments": { "name": "world" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28", // required
"io.modelcontextprotocol/clientInfo": { "name": "my-client", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}, // required
"progressToken": "abc", // optional, opts into a response stream
"io.modelcontextprotocol/logLevel": "info", // optional, same
},
},
}The protocol version and client capabilities are required on every modern request; a request without them is rejected at the boundary. h3-mcp validates and surfaces them on the request context:
handler: (event) => {
const mcp = event.context.mcp!;
mcp.protocolVersion; // "2026-07-28"
mcp.era; // "modern"
mcp.clientInfo; // { name, version, ... } — client SHOULD send this
mcp.clientCapabilities; // {} — what the client can do
mcp.trace; // W3C traceparent / tracestate / baggage, if propagated
};Requiring a capability is one call, and throws -32021 when the client did not declare it:
event.context.mcp!.requireClientCapability?.("sampling");#server/discover
The method is implemented from your handler options — there is nothing to wire up:
// ← result
{
"resultType": "complete",
"supportedVersions": ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26"],
"capabilities": { "tools": {}, "resources": {}, "prompts": {} },
"instructions": "...",
"ttlMs": 3600000,
"cacheScope": "public",
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "my-server", "version": "1.0.0" },
},
}supportedVersions reflects the configured era — a modern-only server lists only 2026-07-28. capabilities is derived from what you declared, and instructions, title, icons, and websiteUrl come from the handler options.
Because discovery output changes rarely, it is worth an explicit cache window:
defineMcpHandler({
name: "my-server",
version: "1.0.0",
cache: { discover: { ttlMs: 3_600_000, cacheScope: "public" } },
});#Echoed Server Info
Every modern result carries _meta["io.modelcontextprotocol/serverInfo"], so a client that reconnected or lost its discovery cache still knows who answered. Opt out if you would rather not repeat it:
defineMcpHandler({ name: "my-server", version: "1.0.0", echoServerInfo: false });#Result Type
Modern results also carry resultType, which is "complete" unless a handler asked the client for more input: