What varies between revisions¶
The adapter surface is derived from this inventory rather than invented ahead of it. Everything here is drawn from the published revisions and verified against a real implementation where it was not obvious.
The inventory¶
Concern |
2026-07-28 |
2025-11-25 / 2025-06-18 |
2025-03-26 |
|---|---|---|---|
Server description |
|
|
same |
Session |
none; state carried by explicit id |
|
same |
|
removed, answered |
standalone SSE stream |
same |
|
required, must equal the |
required after |
not defined; its absence implies this revision |
|
required on every request |
not defined |
not defined |
|
required for |
not defined |
not defined |
|
from |
not defined |
not defined |
Batching |
one message per POST |
one message per POST |
JSON-RPC batch arrays |
Unknown method |
|
|
same |
Header/body mismatch |
|
not defined |
not defined |
Unsupported version |
|
refused at |
refused at |
Missing client capability |
|
not defined |
not defined |
Result envelope |
carries |
none |
none |
Ask the user |
MRTR |
pushed |
no mechanism; see below |
Cache hints |
|
absent |
absent |
Long-lived notifications |
|
standalone |
same |
|
full JSON Schema 2020-12 |
restricted in practice; |
same |
|
unrestricted; |
object-rooted; |
neither exists |
Display |
yes |
yes |
no |
|
yes |
yes |
no |
Logging |
per-request |
|
same |
Cancellation |
close the response stream |
|
same |
2024-11-05 (HTTP+SSE) is a different transport shape entirely – two endpoints
and an endpoint event as the first message – so it is implemented by the
opt-in SseEndpoint, rather than by the Streamable HTTP adapter. Its
handshake selects any revision that still has a handshake; 2026-07-28 is
refused because its required headers cannot travel with HTTP+SSE messages. See
HTTP+SSE.
The three ways to ask¶
This is where the revisions differ most, and where the package earns its keep. Each adapter declares which it has, and everything above the adapter branches on the declaration rather than on a version string.
Flag |
Meaning |
Held by |
|---|---|---|
|
The question is the result; the client calls again with the answers. |
2026-07-28 |
|
The question is pushed on the call’s open stream and answered separately. |
2025-11-25, 2025-06-18 |
|
The question rides in the tool call itself. |
2025-03-26 |
MRTR, on 2026-07-28¶
The server answers with resultType: "input_required", carrying
inputRequests and an opaque requestState. The client calls again with
inputResponses and that state.
The handler runs from the beginning on each attempt. ask returns immediately
for anything already answered, so the handler reads as if it had blocked.
The client sends every answer collected so far on each attempt, not only the newest, because the handler starts over and would otherwise ask the same question forever.
A pushed request, on 2025-11-25 and 2025-06-18¶
The server sends elicitation/create as a genuine server-initiated JSON-RPC
request on the stream of the call in flight, and waits. The client answers with
a plain JSON-RPC response, POSTed on its own.
Nothing pins that answer to the worker that asked. It is published to the hub under a topic named for the question; the waiting worker reads it there. The position is taken before the question goes out, so an answer that arrives before the first poll is still delivered.
Asking where the protocol cannot¶
2025-03-26 predates elicitation. It has nothing to push and no field to carry
a question back in.
What it does have is a tool call, and a caller that reads the result and calls
again. The first call returns an ordinary result – not an error, because the
call did not fail, it is unfinished – whose text names exactly what finishes
it, as JSON to copy. The answers come back as two ordinary arguments,
mcpAnswers and mcpState, taken out before the tool’s own model sees them.
Only a handler that takes the Exchange can ask, so only those tools carry the
two extra properties. Every other tool keeps the schema it would have had, and
the convention costs the model nothing where it cannot be used.
The same result repeats the questions in _meta, under a key named for this
package since no revision specifies one. A model reads the text; a program reads
the meta. That is what lets the bundled client answer them like any other
question.
from pydantic import BaseModel
from aiohttp_tiny_mcp import Client, Exchange, Registry, elicit
from aiohttp_tiny_mcp.protocol.selection import AdapterSet
registry = Registry("gates", "1.0")
class Nothing(BaseModel):
pass
@registry.tool
async def gate(args: Nothing, ex: Exchange) -> str:
"""One confirmation."""
return "went ahead" if (await ex.ask("go", elicit("Go ahead?"))).accepted else "stopped"
@registry.tool
async def plain(args: Nothing) -> str:
"""Asks nothing."""
return "fine"
url = await serve(registry)
adapter = AdapterSet.default().by_version["2025-03-26"]
async with Client(url, adapter) as client:
await client.initialize()
tools = {tool.name: tool for tool in await client.list_tools()}
assert "mcpAnswers" in tools["gate"].input_schema["properties"]
assert "mcpAnswers" not in tools["plain"].input_schema["properties"]
What the client declares¶
A server puts a question only to a client that said it can answer. What “said”
means differs too: a revision with a handshake declares its capabilities once
and the server remembers them; 2026-07-28 restates them on every request and
silence there means no, not “whatever you said last time”.
The argument convention needs no declaration at all. It is not a protocol feature a client opts into – it is a tool call and a caller reading the result, which every client already does.
Degradation that is not a choice¶
Some projection happens because the older revision cannot hold what was declared:
A schema with a nullable union loses the null branch, because a legacy client that cannot read the union would hide the tool entirely.
x-mcp-headerannotations are stripped: a legacy client would not mirror them, and a server that then required the header would deadlock.A schema that cannot be simplified losslessly means the tool is not offered on that revision at all.
Each is one decision in one adapter, visible in one place, rather than a condition in every handler.