The normalized core¶
The types between the adapters and everything else. They are revision-neutral by construction: a field that only makes sense in one revision does not belong here.
Operation¶
The verbs a server can perform. Protocol method names are an adapter concern;
Operation is the internal identity.
from aiohttp_tiny_mcp.core import Operation
assert Operation.CALL_TOOL.value == "call_tool"
Operation |
Reached by |
|---|---|
|
|
|
|
|
|
|
|
|
the |
|
the |
|
|
|
|
|
|
|
|
An adapter may decline to expose an operation, and may map two method names onto one operation.
from aiohttp_tiny_mcp.protocol.selection import AdapterSet
modern = AdapterSet.default().by_version["2026-07-28"]
legacy = AdapterSet.default().by_version["2025-11-25"]
assert modern.method_for(Operation.LISTEN) == "subscriptions/listen"
assert legacy.method_for(Operation.LISTEN) is None
assert legacy.method_for(Operation.SUBSCRIBE) == "resources/subscribe"
assert modern.method_for(Operation.SUBSCRIBE) is None
Call¶
One decoded request, with the revision’s differences already flattened. A POST yields zero of them for a notification the server ignores, one normally, and several for a batch.
It carries the operation, the JSON-RPC id (or None for a notification), the
target, the arguments, the validated params model, who is calling, the progress
token, the log level, any answers already given, and the state left by a
previous attempt.
client is filled from initialize on a revision with a handshake and from
_meta on 2026-07-28. The dispatcher cannot tell which, which is the point.
Outcome¶
What a dispatched operation produced, before encoding. One of three:
Value – a result model.
NeedsInput – the questions a handler needs answered, and the state it left.
Handlers raise NeedInput, which the dispatcher converts; they never build
InputRequiredResult, which is 2026-07-28-specific and lives behind the
adapter.
Failure – a kind from the normalized vocabulary, a message, and optional
data. The adapter maps the kind to its revision’s code and status.
Exchange¶
One request’s worth of everything a handler might need. It arrives by annotation.
|
Put a question, however this revision can |
|
What came back |
|
Report, if the client asked for that severity |
|
Report progress on a streaming call |
|
The session a handshake opened, or |
|
Open or reach one by handle, on any revision |
|
What the previous attempt left |
|
Who is calling |
|
The aiohttp request, for what middleware decided |
|
Cancellation |
It also resolves dependencies, and closes anything scoped to the request when the request ends.
Dispatcher¶
Routes an Operation to its handler with a match over every member, so a
renamed method is a type error rather than a silent “unknown method” at runtime.
It is also where the two things that wrap every call happen: the state a client
carried is restored before dispatch and the state a handler leaves is stored
after it, and a NeedInput that no client can answer becomes the failure the
specification names.
Endpoint¶
The transport. It selects the adapter, decodes, decides whether the reply is one JSON body or a stream, dispatches, and encodes.
A reply is streamed when the call has something to send before its result: a
tool declared streaming=True, a subscriptions/listen that runs until the
client goes away, or a question this revision would have to push.
GET is the notification stream for the revisions that read one. DELETE is
not defined – nothing here ends a session on demand; one ends when it expires.
Caching¶
List results depend on the registry and the revision, not on the call, so they
are rendered once per adapter and reused. Registry invalidates the cache when
something is declared.
This is why Value is a model rather than bytes: the adapter renders, the
registry caches what was rendered, and the two stay separable.