Tools, resources, and prompts¶
MCP connects a client — usually an application hosting a model — to your server. Your server offers one or more of these:
Use this when you offer… |
Declare |
Example |
|---|---|---|
An operation with arguments |
A tool |
Search a catalog; create a deployment |
Read-only content at an address |
A resource |
|
A reusable conversation starting point |
A prompt |
“Review this incident” |
A tool is the default. Its Pydantic input model becomes an input schema; its docstring tells a client when to call it. A resource is addressed by a URI, not called with arbitrary arguments. A prompt returns messages for a client to show or use in a conversation.
@registry.tool
async def search(args: Search) -> Results:
"""Search the catalog by text."""
...
See Tools, Resources, and Prompts for the APIs and result types.
What you declare¶
The server’s public surface consists of a registry and its handlers:
Declare |
The client receives |
|---|---|
|
Server identity and instructions |
Tool function, model, and docstring |
Name, description, input schema, and result |
Resource URI and handler |
A readable URI or URI template |
Prompt function and model |
A named prompt and its arguments |
Exchange, database clients, the store, and the hub are server-side objects.
They are never tool arguments and are not visible in a tool listing.
MCP and this library¶
MCP defines the messages on the wire: JSON-RPC, tools, resources, prompts, transports, capabilities, and notifications. This library maps those messages to aiohttp and Python:
MCP defines |
This library supplies |
|---|---|
Tool/resource/prompt messages |
Pydantic handlers and decorators |
Streamable HTTP and stdio |
|
Progress, logging, and elicitation |
|
Sessions and notifications |
|
Protocol revisions |
Five adapters behind one handler API |
The protocol does not choose a database, persist a Python session, or deliver an event between two application workers. That is the remote-web-mcp problem this library addresses. How the server fits together explains the two contracts used for it.
Official Python SDK¶
The official Python SDK
already provides a high-level server API: FastMCP in 1.x and MCPServer in
2.x. Its broad feature set is useful.
Both serve the same five protocol revisions from one handler set.
Revisions |
the same five |
|---|---|
Covered dates |
|
Why this library exists¶
For a server behind a load balancer, the SDK has the wrong state boundary.
Its stateful Streamable HTTP session belongs to the process that created
it. A request routed to another worker gets Session not found: a stateful HTTP session does not move,
and the SDK exposes no backend seam for sharing it.
This is not imposed by JSON-RPC or by Streamable HTTP. A JSON-RPC request is self-contained; for the older MCP revisions, its session ID can simply select a shared record. Keeping the transport session in one worker’s memory is an SDK design choice. It turns stateless serving into a special mode rather than the normal deployment model.
The SDK can run stateless HTTP across workers, but this is an escape hatch:
it gives up the server-side session. This is sufficient for 2026-07-28, which
has no handshake session. It is not sufficient for a cross-protocol server that
must serve the older revisions with negotiated capabilities, log level, and
subscriptions without sticky routing. The alternatives are affinity, which
defeats ordinary load balancing, or dropping that state.
The SDK can distribute question state and change events. Its question state is encrypted and carried by the client; workers share a key. A shared pub/sub can fan change events out across replicas. Neither mechanism provides shared legacy sessions.
This library puts all three concerns in application backends: SessionStore
holds legacy sessions and opaque request state, and Hub carries replies and
notifications. A handler keeps the same API on all five revisions, while any
worker can handle the next request; nothing leaves the server except opaque
identifiers.
The SDK is also disproportionately slow before application work begins. The
local benchmark normalizes each HTTP stack against its bare framework and still
measures about 3–4× the tools/call rate here; over stdio, where no web
framework is involved, it is about 4–5×. Its stateless mode is slower than its
stateful mode on legacy revisions because it creates and destroys a session for
each request. These are small-handler measurements, not production capacity
claims; see What it costs.
Choose deliberately¶
Choose the SDK when sampling, roots, middleware, or extensions matter, or when
Starlette/ASGI is a fixed constraint. This library provides bearer-token
verification for an OAuth protected resource; see Authentication.
Choose it for aiohttp and for legacy session-based MCP behind a load balancer.
The distributed claims are exercised in tests/test_cluster.py.