This is the best story in the series, and it is really a story about a habit: reading what is installed instead of what is written about it.
Part 1 · The obvious way to write it
Sampling, in every tutorial, article and example you will find, is one line:
const answer = await server.server.createMessage({ ... }); // ❌The server pushes a request down the connection to the client, the client runs the model, the answer comes back up. It reads beautifully. It is in the types. It has autocomplete.
It throws.
Part 2 · The habit — read the types on disk first
Before building anything, go and look at what is actually installed. This is the fifth consecutive project where doing so changed the design, and this time it changed it most.
npm ls @anthropic-ai/sdk @modelcontextprotocol/server mcp-handler
grep -rn "createMessage\|registerResource\|registerPrompt\|elicitInput" \
node_modules/@modelcontextprotocol/server/dist/*.d.mts | headThe types say sampling exists. server.server.createMessage(),
registerResource, registerPrompt, elicitInput — all there, all typed. You
could stop here and start building, and you would have a working plan and a
lost week.
Don't. Every one of them is marked @deprecated, with this beside it:
the 2026-07-28 revision has no server→client request channel
So check the constants
node --input-type=module -e "
import { LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/server';
console.log(LATEST_PROTOCOL_VERSION, JSON.stringify(SUPPORTED_PROTOCOL_VERSIONS));"2025-11-25 ["2025-11-25","2025-06-18","2025-03-26","2024-11-05","2024-10-07"]
No 2026-07-28 anywhere. A constant literally named LATEST_PROTOCOL_VERSION
does not contain the latest protocol version.
Part 3 · The probe
Checkpoint 1 — the naive version fails, and explains itself
{"result":{"content":[{"type":"text","text":
"Cannot request input 'think' (sampling/createMessage): the client on this
2025-era connection did not declare the required capability (no client
capabilities are available on this connection — per-request legacy serving
cannot receive server-to-client requests)"}],"isError":true}}Read the last clause slowly, because it is the whole finding:
per-request legacy serving cannot receive server-to-client requests
Checkpoint 2 — a second era, hiding behind an envelope
SUPPORTED_PROTOCOL_VERSIONS only describes the old initialize handshake.
The modern era does not negotiate at connection time at all, because there is
no connection. It travels per request, in a _meta envelope.
META='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"probe","version":"1"},
"io.modelcontextprotocol/clientCapabilities":{"sampling":{}}}'
curl -s -X POST http://localhost:3000/api/probe \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Mcp-Method: tools/call' -H 'Mcp-Name: probe_sampling' \
-d "{\"jsonrpc\":\"2.0\",\"id\":6,\"method\":\"tools/call\",
\"params\":{\"name\":\"probe_sampling\",\"arguments\":{},$META}}"Same server. Same tool. Different era. And this time:
{"result":{
"resultType":"input_required",
"inputRequests":{"think":{"method":"sampling/createMessage","params":{…}}},
"requestState":"…"}}There it is. Sampling is not a callback. It is a retry.
Part 4 · The shape that replaced it
On the wire:
round 1 → tools/call
← {"resultType":"input_required",
"inputRequests":{"summary":{"method":"sampling/createMessage",…}},
"requestState":"…"}
· the host runs the model
round 2 → tools/call + params.inputResponses + params.requestState
← {"resultType":"complete","content":[…]}A retry needs no connection at all. Which is precisely why they changed it.
This is now the specification's position, in MUST language
Project #5 found this by probing. The spec states it outright:
Servers MUST send server-to-client requests (such as
roots/list,sampling/createMessage, orelicitation/create) using the MRTR pattern. The previous pattern of server-initiated requests is no longer supported. This is a breaking change.
And it applies to more than sampling. roots/list and elicitation/create use
the identical mechanism — the pattern is called multi round-trip requests,
and it is the general answer to "how does a server ask for anything?"
Servers may only return input_required on three requests: tools/call,
resources/read, and prompts/get.
Part 5 · Three things that cost a round trip each
All silent failures. Everything looks fine; nothing happens.
| Symptom | Cause |
|---|---|
-32020 "the request headers and body disagree" | the 2026 era needs an Mcp-Method header, and an Mcp-Name when params carry a name or uri |
-32021 "client capabilities do not declare the required capability" | you didn't put sampling: {} in the envelope. This is why the danger is opt-in. |
| the handler asks for the same thing forever | acceptedContent() is elicitation-only. Sampling needs inputResponse(responses, key) → {kind:"sampling", result} |
That third one is worth expanding, because it is an infinite loop that costs
money. The two readers are not even the same shape: a sampling view wraps its
payload in .result, while an elicitation view is flattened — action and
content sit directly on it. Use the wrong reader and you get undefined,
which is indistinguishable from "no answer yet", so the handler asks again.
And each ask is a full model call.
And one placement detail that is pure trap: inputResponses and requestState
are top-level members of params — not inside _meta, where the rest of
the modern-era machinery lives. Put them in _meta and the server never sees
them, so it asks again. It looks like a loop bug. It is a placement bug.
Part 6 · Why this counts as a finding
The whole thing was discovered by writing a throwaway server and curling it — not by remembering how sampling works, and not by reading a guide.
A deprecation notice on the thing you were about to build on is worth more than a working example of it.
That is the transferable part. A working example tells you that something
used to work in somebody's environment. A deprecation notice tells you what
the maintainers know about where it is going — and it is sitting in your
node_modules, for free, with the reason attached.
What you now know
- Every sampling tutorial shows
createMessage(), a server→client push. - A push needs a connection; a serverless function has a request and then it doesn't. The SDK's own error says exactly this.
LATEST_PROTOCOL_VERSIONdescribes only the handshake-negotiated era, so it does not list2026-07-28.- The replacement is a retry:
input_required→ you do the work → call the same tool again withinputResponsesandrequestState. - That pattern (MRTR) is now a
MUST, is a documented breaking change, and covers roots and elicitation too. inputResponsesandrequestStatego at the top level ofparams.- When types and constants disagree, the wire is the tiebreaker.