Registries¶
Tool, resource, and prompt lookup, plus session storage and SSE infrastructure.
ToolBinding wraps a ServiceSpec (mutation tools);
SelectorToolBinding wraps a SelectorSpec and exposes the read-shaped
pipeline knobs — filter_set is read from the spec, and carries ordering
with it via an OrderingFilter (a selector declaring its own sort
parameter is the other ordering route, and likewise needs no knob);
paginate is the one binding-level MCP mechanic. The shared
ToolRegistry accepts either kind and is what tools/list and
tools/call iterate.
ToolBinding
dataclass
¶
Bases: Generic[InputT, ResultT, ExtraT]
All wiring for a single MCP tool, derived from a ServiceSpec.
A tool is the projection of a service callable plus its declared input and
output serializers. The MCP server invokes spec.service directly via
resolve_callable_kwargs + run_service — there is no view or viewset
in the dispatch path.
annotations and meta are emitted verbatim on this tool's
tools/list entry, under annotations and _meta respectively.
The generic parameters mirror ServiceSpec's and are purely
informational for type checkers, defaulting to Any when omitted.
display_name
class-attribute
instance-attribute
¶
Consumer-only label, never emitted on the MCP wire, so a downstream
library can render a richer label than the protocol title.
display_description
class-attribute
instance-attribute
¶
Consumer-only blurb, the sibling of display_name and likewise
never emitted on the MCP wire.
icons
class-attribute
instance-attribute
¶
Display icons, emitted in this tool's listing entry. Purely presentational; nothing in dispatch reads them.
include_structured_content
class-attribute
instance-attribute
¶
Whether this tool's tools/call response carries
structuredContent. None defers to the
INCLUDE_STRUCTURED_CONTENT setting.
include_output_schema
class-attribute
instance-attribute
¶
Whether this tool's tools/list entry carries an outputSchema.
None defers to the INCLUDE_OUTPUT_SCHEMA setting.
The MCP spec forbids advertising outputSchema while suppressing
structuredContent, so True together with
include_structured_content=False is rejected at construction.
max_result_bytes
class-attribute
instance-attribute
¶
Per-tool outbound result ceiling. UNSET defers to the server's
MAX_RESULT_BYTES, None disables the check for this tool, an int
sets its own. Sentinelled rather than tri-state because None here means
"no ceiling", which a deliberately-large export tool genuinely wants.
dispatch_timeout
class-attribute
instance-attribute
¶
Per-tool dispatch deadline, in seconds. UNSET defers to the server's
DISPATCH_TIMEOUT, None disables it here. Async transport only — see
dispatch_timeout.
argument_binding
class-attribute
instance-attribute
¶
How MCP arguments flow into the kwarg pool. BUNDLE for service
tools, because a mutation service typically takes one
input_serializer-validated data payload and spreading the dict as
top-level kwargs would conflict with that shape.
unknown_arguments
class-attribute
instance-attribute
¶
How unknown arguments keys are handled relative to the binding's
inputSchema.
REJECT(default) answers-32602and advertisesadditionalProperties: false— but only with aninput_serializerto validate against. A serializer-less binding has no declared field set, soREJECTcannot fire and its schema stays open.PASSTHROUGHadvertises an open schema and merges unknown keys into the validated payload.IGNOREadvertises an open schema and drops them.
always_listed
class-attribute
instance-attribute
¶
Keep this binding in tools/list even when
FILTER_LISTINGS_BY_PERMISSIONS would drop it because its
permissions deny the caller. A discovery aid for tools the caller can
see but not invoke — tools/call still 403s.
query_params
class-attribute
instance-attribute
¶
Read-shaping params routed to request.query_params at dispatch.
Popped from the caller's arguments like a URL kwarg, but landing in the
synthetic request's GET rather than view.kwargs — the channel a
serializer reads when it branches on the query string. A filter_set
field is not one of these.
url_kwargs
class-attribute
instance-attribute
¶
URL-derived values the model supplies as tool args, seeded into the
off-HTTP view's kwargs instead of reaching the service as ordinary
params, so a scoping spec.kwargs provider reading view.kwargs sees
them. Advertised in the inputSchema and stripped from the dispatched
params. See UrlKwarg.
content_kind
class-attribute
instance-attribute
¶
What this tool's payload becomes in the result's content array. TEXT
renders JSON per output_format; the other kinds project it into an image / audio
/ resource-link block. See
ToolContentKind.
content_mime_type
class-attribute
instance-attribute
¶
The media type for an IMAGE / AUDIO content_kind.
Required for those and meaningless for the rest — a resource link carries
its own mimeType per entry.
task_policy
class-attribute
instance-attribute
¶
Whether calling this tool hands back a task handle instead of a result.
The choice lives on the binding because the extension makes the server
the sole decider and gives the client no way to ask. See
TaskPolicy.
invalidates
class-attribute
instance-attribute
¶
URI templates naming the resources a successful call changed.
Published as notifications/resources/updated once the transaction
commits, so subscribers re-read. Same {var} syntax as a resource's
uri_template, rendered against the result merged with the call's
arguments:
invalidates=("invoices://{pk}", "invoices://")
Name the collection too if you want it watched. Topic matching is
exact — a prefix rule would match invoices://1 against
invoices://11 and miss a tenant-scoped scheme entirely.
Only calls that go through this server fire it. A management command, a
Celery job or an admin edit changes the same rows and publishes nothing;
MCPServer.notify_resource_updated covers those.
field_audiences
class-attribute
instance-attribute
¶
Per-tool overrides layered over the FieldMarking declarations the
output serializer carries on its own fields.
The serializer stays authoritative — it is the one declaration the REST API, this transport, and an in-process toolset all read. This exists for the case one tool genuinely needs what a sibling hides: a lookup tool returning the identifier its neighbour drops.
Declared on the registry entry's
OfflineContract
and resolved here, so the field set an agent sees does not depend on which
agent transport served it.
output_serializer
property
¶
The serializer whose rendered output reaches the caller, if any.
audience_projection
cached
property
¶
This tool's resolved audience markings, derived once per binding.
Drives both the projected payload and the advertised outputSchema,
so the two cannot disagree about which fields a caller will receive.
SelectorToolBinding
dataclass
¶
Bases: Generic[ResultT, ExtraT]
All wiring for a single MCP read-shaped tool, from a SelectorSpec.
The read-shaped mirror of
ToolBinding.
Selectors return raw, unscoped data and the tool layer owns every shape decision,
chosen by kind.
kind=LIST runs the full pipeline:
arguments → validate(merged inputSchema) → run_selector
→ FilterSet(data=...).qs (if ``filter_set`` set, and it
orders too when it declares an
``OrderingFilter``)
→ paginate (if ``paginate=True``)
→ output_serializer(many=True)
→ ToolResult
With neither set it behaves as a plain RPC read, rendering the selector's return value verbatim.
kind=RETRIEVE skips pagination but still applies queryset shaping and
spec.filter_set before materializing the instance via .first() — so
a "stats from a filtered set" retrieve works — then renders
output_serializer(many=False). Pairing it with paginate is rejected
at construction: that knob only means something on a collection.
paginate=True generates page / limit arguments, slices the
queryset and wraps the response with items / page / totalPages
/ hasNext. Ordering has no binding-level knob at all, and two channels
that need none: an OrderingFilter on the spec's FilterSet, or a sort
parameter the selector declares for itself. Both are reflected into the
inputSchema, so one declaration serves the HTTP transport and every
agent transport alike. Prefer the filter where there is one — it validates
the value against published choices before it reaches the ORM, while a bare
parameter is only as safe as what the selector does with it — and do not
name that parameter ordering / page / limit, which
RESERVED_POST_FETCH_KEYS strips from the selector's pool.
annotations and meta are emitted verbatim on this tool's
tools/list entry, under annotations and _meta respectively.
The generic parameters mirror SelectorSpec's and are purely
informational for type checkers.
display_name
class-attribute
instance-attribute
¶
Consumer-only label, never emitted on the MCP wire, so a downstream
library can render a richer label than the protocol title.
display_description
class-attribute
instance-attribute
¶
Consumer-only blurb, the sibling of display_name and likewise
never emitted on the MCP wire.
input_serializer
class-attribute
instance-attribute
¶
Custom non-filter tool arguments, declared MCP-side.
SelectorSpec carries no input serializer of its own: a selector only
describes how to fetch, and the HTTP transport validates the URL and query
separately. MCP has no such split — every tool call is one arguments
dict — so arguments that are not filter / ordering / pagination knobs are
declared here.
icons
class-attribute
instance-attribute
¶
Display icons, emitted in this tool's listing entry. Purely presentational; nothing in dispatch reads them.
include_structured_content
class-attribute
instance-attribute
¶
Whether this tool's tools/call response carries
structuredContent. None defers to the
INCLUDE_STRUCTURED_CONTENT setting.
include_output_schema
class-attribute
instance-attribute
¶
Whether this tool's tools/list entry carries an outputSchema.
None defers to the INCLUDE_OUTPUT_SCHEMA setting.
The MCP spec forbids advertising outputSchema while suppressing
structuredContent, so True together with
include_structured_content=False is rejected at construction.
max_result_bytes
class-attribute
instance-attribute
¶
Per-tool outbound result ceiling. UNSET defers to the server's
MAX_RESULT_BYTES, None disables it here, an int sets its own.
dispatch_timeout
class-attribute
instance-attribute
¶
Per-tool dispatch deadline, in seconds. UNSET defers to the server's
DISPATCH_TIMEOUT, None disables it here. Async transport only.
max_page_size
class-attribute
instance-attribute
¶
Per-tool ceiling on the model-supplied limit. UNSET defers to the
server's MAX_PAGE_SIZE, None serves any limit the model asks
for.
Only meaningful with paginate=True: an unpaginated selector has no
limit to clamp, and clamping its result would drop rows with nothing in
the payload to say so (see UnboundedListWarning).
argument_binding
class-attribute
instance-attribute
¶
How MCP arguments flow into the kwarg pool. SPREAD_AUTHOR_WINS
for selector tools, because a selector typically declares its query
parameters as individual function arguments
(def list_drafts(*, project_id, page=1, limit=10)).
unknown_arguments
class-attribute
instance-attribute
¶
How unknown arguments keys are handled relative to the merged
inputSchema (input_serializer fields, filter_set properties,
ordering, pagination). REJECT answers -32602, PASSTHROUGH
merges them into the validated payload, IGNORE drops them.
always_listed
class-attribute
instance-attribute
¶
Keep this binding in tools/list even when FILTER_LISTINGS_BY_PERMISSIONS
would drop it — same semantics as
ToolBinding.always_listed.
query_params
class-attribute
instance-attribute
¶
Read-shaping params routed to request.query_params at dispatch.
Popped from the caller's arguments like a URL kwarg, but landing in the
synthetic request's GET rather than view.kwargs — the channel a
serializer reads when it branches on the query string. A filter_set
field is not one of these.
url_kwargs
class-attribute
instance-attribute
¶
URL-derived values the model supplies as tool args, seeded into the off-HTTP
view's kwargs instead of reaching the selector as ordinary params. Advertised in
the inputSchema, exempt from the unknown-argument check, and stripped from the
dispatched params. See
UrlKwarg.
content_kind
class-attribute
instance-attribute
¶
What this tool's payload becomes in the result's content array. TEXT
renders JSON per output_format; the other kinds project it into an image / audio
/ resource-link block. See
ToolContentKind.
content_mime_type
class-attribute
instance-attribute
¶
The media type for an IMAGE / AUDIO content_kind.
Required for those and meaningless for the rest — a resource link carries
its own mimeType per entry.
task_policy
class-attribute
instance-attribute
¶
Whether calling this tool hands back a task handle instead of a result.
The choice lives on the binding because the extension makes the server
the sole decider and gives the client no way to ask. See
TaskPolicy.
field_audiences
class-attribute
instance-attribute
¶
Per-tool overrides layered over the FieldMarking declarations the
output serializer carries on its own fields.
The serializer stays authoritative — it is the one declaration the REST API, this transport, and an in-process toolset all read. This exists for the case one tool genuinely needs what a sibling hides: a lookup tool returning the identifier its neighbour drops.
Declared on the registry entry's
OfflineContract
and resolved here, so the field set an agent sees does not depend on which
agent transport served it.
output_serializer
property
¶
The serializer whose rendered output reaches the caller, if any.
audience_projection
cached
property
¶
This tool's resolved audience markings, derived once per binding.
Drives both the projected payload and the advertised outputSchema,
so the two cannot disagree about which fields a caller will receive.
kind
property
¶
Shape discriminator, read from the spec's required kind field.
Not stored independently on the binding: a second copy would only be a chance for the two to drift.
filter_set
property
¶
Transport-neutral filtering, read from SelectorSpec.filter_set.
Delegated rather than copied, like kind and selector,
so a project declares its filterable shape once on the spec and both
the HTTP and MCP transports honour it.
Typed Any because django-filter is optional behind the
[filter] extra, and narrowing would force a hard import here.
UrlKwarg
dataclass
¶
A URL route capture exposed as a caller-supplied argument off-HTTP.
Over HTTP a nested route's captures (the project_pk of
/projects/{project_pk}/widgets/) reach a spec through view.kwargs —
directly, or through a spec.kwargs provider that scopes by them. Off-HTTP there
is no route, so the caller supplies the value as an ordinary argument: the transport
advertises it in the tool / operation schema, pops it out of the arguments, and
hands it to build_offline_context(kwargs=…), from where
dispatch_spec
spreads it into the selector / target pools — authoritative over the spec params,
below a spec.kwargs provider. It never reaches the spec as an ordinary input, so
the unknown-argument policy never flags it. Adapters import this declaration and
pair it with
validate_channel_names.
Reach for one when the value is a URL-derived input a spec depends on that is
not already an ordinary argument — most commonly a scoping spec.kwargs
provider reading view.kwargs (off-HTTP that mapping is otherwise empty, so
the provider mis-scopes for every caller), or a closed-surface spec whose route
capture must be caller-suppliable. A selector reading the value from its own
**extras: Unpack[TypedDict] needs no UrlKwarg: drf-services reflects
the key into the schema and params delivers it. A key can be both
reflected and registered — the explicit UrlKwarg wins the adapter's schema
merge, registration pops the argument into kwargs=, and the authoritative
spread still delivers it to the selector pool, so both readers see it.
An explicit null from the caller is not a supplied value. A route capture
is a URL segment, so over HTTP there is no value that means null; off-HTTP,
{"project_pk": null} is what a model emits for a capture it could not
resolve. A transport treats that as an omitted argument — required still
refuses it and default still applies — rather than spreading None onto
view.kwargs, where a scoping provider would turn it into a silent
IS NULL lookup that returns rows and looks successful.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The argument / view-kwarg key. Must not collide with a reserved
transport key; see
|
type |
str
|
The JSON-Schema type advertised to the caller — |
description |
str | None
|
Optional help text shown to the caller. |
default |
Any
|
Value seeded when the caller omits the argument; also surfaced as
the schema |
required |
bool
|
Advertise the key in the schema's |
json_schema ¶
The JSON-Schema property this kwarg contributes to an input schema.
default is emitted whenever one was declared — UNSET is the
"no default" sentinel, so an explicit default=None reaches the
schema as "default": None instead of vanishing.
QueryParam
dataclass
¶
A request-level query param exposed as a caller-supplied argument off-HTTP.
Generalizes the built-in page / limit / order list-selector
arguments to any read-shaping param a serializer reads off
request.query_params — django-restql field selection (?query= /
?fields=), or a custom serializer that branches on the query string. The
transport advertises it, pops it from the arguments, and hands it to
build_offline_context(query_params=…); it never reaches the spec as an
input, so the unknown-argument policy never flags it.
A SelectorSpec
filter_set does not need this — its fields are already generated into the
schema and flow through as ordinary params.
Declared here rather than in each adapter for the same reason as
UrlKwarg: it is the same
declaration whichever transport carries it. Pair it with
validate_channel_names.
name— the argument / query-string key. Must not collide with a reserved transport key; seevalidate_channel_names.type— the JSON-Schema type advertised to the caller ("string"by default;"integer"/"number"/"boolean"/"array"…).description— optional help text shown to the caller.default— value seeded when the caller omits the argument; also surfaced as the schemadefault. Left atUNSETthere is no default, and the schema carries nodefaultkey;default=Noneis a real declaration ("defaults to null") and is surfaced like any other value. Read it withis not UNSET, never with a truthiness oris not Nonetest.
An explicit null from the caller is not a supplied value. Over HTTP a
query param is always a string, so there is no value a caller can send that
means null; off-HTTP, {"fields": null} is the shape a model emits for a
param it chose not to fill. A transport treats it as an omitted argument —
the default still applies — rather than routing None onto
request.query_params.
No required flag, deliberately. A query param is read-shaping —
omitting one is legitimate by construction, and the spec runs correctly
without it. Requiredness belongs to inputs the spec cannot run without, which
is UrlKwarg and the
InputRequired marker.
json_schema ¶
The JSON-Schema property this param contributes to an input schema.
default is emitted whenever one was declared — UNSET is the
"no default" sentinel, so an explicit default=None reaches the
schema as "default": None instead of vanishing.
ToolRegistry ¶
Name to tool binding lookup.
Holds service, selector and chain bindings in one namespace, rejecting duplicates loudly so a misconfigured project surfaces the conflict at registration rather than silently shadowing a tool.
all ¶
Every binding, in registration order.
tools/list has to be deterministic so clients can cache the catalog
and cursor pagination stays stable, which dict insertion order already
gives. Deliberately not sorted by name: registration order is authored
order, which is a better first page for a model than an alphabetical
one.
ResourceBinding
dataclass
¶
Bases: Generic[ResultT]
All wiring for a single MCP resource (or resource template).
A resource is a selector callable plus a URI template. The MCP server
invokes the selector directly via resolve_callable_kwargs +
run_selector — there is no view or viewset in the dispatch path.
output_serializer is what resources/read renders the selector's
return value through, and mime_type advertises the type that body will
carry. kwargs_provider mirrors SelectorSpec.kwargs: when set, the
handler invokes it once per request and merges the returned dict into the
kwarg pool, passing a synthesised
OfflineServiceView whose view.kwargs
holds the URI-template variables and whose view.action is the binding
name. annotations and meta are emitted verbatim on the listing
entry, and meta also on the contents block of resources/read.
The generic parameter is purely informational, letting callers pin the selector's return type for type-checker help.
kind
instance-attribute
¶
Pulled out of SelectorSpec.kind by the adapter, so the binding does
not carry a reference to the whole spec. LIST invokes the output
serializer with many=True, RETRIEVE (the common case for
URI-template resources) with many=False. Resources have no post-fetch
pipeline, so both kinds are accepted unconditionally.
encoding
class-attribute
instance-attribute
¶
How the selector's value becomes the resources/read body. Declared rather
than inferred from mime_type, so advertising a new mime type never silently
changes the encoding. See
ResourceEncoding.
completions
class-attribute
instance-attribute
¶
Argument name to completer callable, powering completion/complete.
A completer is dispatched through resolve_callable_kwargs against a
pool of value (the text typed so far), arguments (siblings the
client has already resolved, also spread by name), request and
user. It returns an iterable of suggestions — a list, a generator or a
queryset — which the handler slices to the spec's cap rather than
draining.
cache_ttl_ms
class-attribute
instance-attribute
¶
How long a client may cache this resource's body, in milliseconds.
UNSET takes the server's RESOURCE_CACHE_TTL_MS, 0 by default
because a resource body is live data. Worth setting on anything genuinely
static: hosts prefetch interactive views before any tool call, so a zero
TTL means fetching the same HTML repeatedly.
icons
class-attribute
instance-attribute
¶
Display icons, emitted in this resource's listing entry. Purely presentational; nothing in dispatch reads them.
always_listed
class-attribute
instance-attribute
¶
Keep this resource in resources/list (or resources/templates/list) even
when FILTER_LISTINGS_BY_PERMISSIONS would drop it — same semantics as
ToolBinding.always_listed.
ResourceRegistry ¶
URI or URI-template to
ResourceBinding
lookup.
Concrete resources are matched by exact URI, templates by a regex derived
from the template. resolve returns the binding plus the variables
extracted from the URI.
Specificity, not registration order. A template's {var} matches any
single segment, so reports://{report_id} also matches
reports://all-tenants-summary. Resolving in registration order would
make which permission stack guards a URI a function of the order the two
were registered in — and the wrong answer is the permissive one, since the
template is the general case. Concrete URIs are therefore tried first, and
only then templates.
by_uri_template ¶
Exact lookup on the registered template string.
A caller holding the template itself — the completion API's
ref/resource, say — must use this rather than resolve:
things://{pk} satisfies its own pattern with pk="{pk}", so
resolve would answer, plausibly and wrongly.
ResourceEncoding ¶
Bases: str, Enum
How a resource's selector return value becomes the resources/read body.
Declared separately from the binding's mimeType rather than sniffed
from it — sniffing would silently change behaviour for anyone already
advertising a non-JSON type.
Attributes:
| Name | Type | Description |
|---|---|---|
JSON |
Pretty-print the value as JSON. The default, and what every selector-backed data resource wants. |
|
TEXT |
The value is already the body; the selector must return a
|
|
BLOB |
The value is binary. The selector returns |
PromptBinding
dataclass
¶
All wiring for a single MCP prompt.
A prompt is a server-defined message template the client invokes by name. The
render callable receives the client-supplied arguments as kwargs and returns a
list of
PromptMessage
instances, a list of strings (each becoming a user text message), a single string,
or a coroutine yielding any of those. The handler normalises whichever shape arrives
into the spec's messages list at dispatch time.
annotations and meta are emitted verbatim on this prompt's
prompts/list entry, under annotations and _meta respectively.
completions
class-attribute
instance-attribute
¶
Argument name to completer callable, powering completion/complete.
A completer is dispatched through resolve_callable_kwargs against a
pool of value (the text typed so far), arguments (siblings the
client has already resolved, also spread by name), request and
user. It returns an iterable of suggestions — a list, a generator or a
queryset — which the handler slices to the spec's cap rather than
draining.
icons
class-attribute
instance-attribute
¶
Display icons, emitted in this prompt's listing entry. Purely presentational; nothing in dispatch reads them.
always_listed
class-attribute
instance-attribute
¶
Keep this prompt in prompts/list even when FILTER_LISTINGS_BY_PERMISSIONS
would hide it — same semantics as
ToolBinding.always_listed.
PromptRegistry ¶
Name to
PromptBinding
lookup.
Mirrors ToolRegistry:
names are unique and a duplicate raises at registration.
Interactive views (MCP Apps)¶
MCPServer.register_ui_resource(...) declares an HTML view for an MCP host to
render inline in the chat. The view is an ordinary ResourceBinding with the
Apps mime type, TEXT encoding, and a _meta bundle built from
UIResourceMeta; UIToolMeta then links a tool to it, so the host renders that
tool's result inside the view. See
Interactive views for the
host/server split, the three refused-link cases, and the keep-tenant-data-out
rule.
UIResourceMeta
dataclass
¶
What a host needs to know to render an interactive view.
Serialises into the resource's _meta under the Apps extension's key.
Typed at the registration parameter rather than in the wire types, because
_meta is an open namespace shared by every extension: it stays a
free-form dict at the boundary while each extension keeps its own closed
shape on the way in.
Attributes:
| Name | Type | Description |
|---|---|---|
csp |
UICsp | None
|
Origins the view needs; see
|
permissions |
Sequence[UIPermission]
|
Browser capabilities the view would use. The host decides whether to grant them. |
domain |
str | None
|
A stable identity for the view's origin, letting a host group views from one publisher — for a single consent prompt, say — rather than treating every URI as unrelated. |
prefers_border |
bool | None
|
Rendering hint that the view looks better with the host's chrome around it. A hint, not a requirement. |
to_dict ¶
Serialise to the extension's camelCase wire shape, omitting empties.
UICsp
dataclass
¶
The network origins an interactive view needs, declared to the host.
The server declares; the host enforces. A host builds the iframe's Content-Security-Policy from this, so an origin the view talks to but does not declare here is blocked at runtime, with nothing in the server logs to say so.
Each field is a sequence of origins ("https://api.example.com") mapping
onto one CSP directive. An empty one is omitted from the payload, so
declaring nothing declares nothing — which is not the same as declaring
"deny all", the host's default anyway.
Attributes:
| Name | Type | Description |
|---|---|---|
connect_domains |
Sequence[str]
|
|
resource_domains |
Sequence[str]
|
Images, stylesheets, scripts, fonts. A view loading
Django |
frame_domains |
Sequence[str]
|
Origins the view may itself embed in an iframe. |
base_uri_domains |
Sequence[str]
|
Permitted values for the document's |
to_dict ¶
Serialise to the extension's camelCase wire shape, omitting empties.
UIPermission ¶
Bases: str, Enum
A browser capability an interactive view asks the host to grant.
The host decides; this only declares what the view would use, in the
resource's _meta.ui.permissions. Anything not declared is denied by the
iframe sandbox the host builds.
UIToolMeta
dataclass
¶
Links a tool to the interactive view that renders its result.
Serialises into the tool's _meta under the Apps extension's key, so a host
reading tools/list knows which ui:// resource to fetch and which surfaces
may call the tool. The view renders from the tool's structuredContent, so a
linked tool must emit it:
register_service_tool
and friends refuse a link when it is switched off.
Attributes:
| Name | Type | Description |
|---|---|---|
resource_uri |
str
|
The |
visibility |
Sequence[UIVisibility]
|
Who may call the tool. Empty is "unsaid", which hosts read
as the ordinary model-callable default. Host-enforced: this
server declares it and does not filter |
to_dict ¶
Serialise to the extension's camelCase wire shape, omitting empties.
UIVisibility ¶
Bases: str, Enum
Who may call a tool that is linked to an interactive view.
Declared per tool in _meta.ui.visibility and enforced by the host,
which the spec requires not to offer the model a tool whose visibility
omits MODEL. This server only declares it — nothing here filters
tools/list on it, because a client that does not implement the
extension would not honour the rule anyway.
Attributes:
| Name | Type | Description |
|---|---|---|
MODEL |
The agent may call it — ordinary tool behaviour. |
|
APP |
The view may call it. An |
The view document¶
register_ui_resource(body_template_name=...) composes the document through
this, so most projects never call it directly. It is exported for the case that
assembles the markup some other way and still wants the packaged shell and the
ui/* bridge — a bridge whose failure modes are all silent, and which
Writing the bridge yourself
describes for anyone declining it.
build_app_document ¶
Wrap a view's markup in a complete MCP Apps document.
body is a fragment -- the view's own markup, styles and scripts, with
no <html> around it. What comes back is the whole document: the element
structure a sandbox expects, a minimal theme-inheriting stylesheet, and the
ui/* postMessage bridge inlined ahead of the fragment so the fragment can
assign mcpApp.onToolResult while the parser is still running.
Reached most easily through register_ui_resource(body_template_name=...),
which renders a Django template into body. It is public because the other
content sources deserve the same shell: a project assembling its markup some
other way can wrap the result and pass it as html= or return it from a
selector=.
Three properties are structural rather than advisory, because each of them cost a consumer at least one debugging round:
<html>,<head>and<body>are written out. HTML5 infers all three and browsers do not care, but the sandbox loads this document as raw HTML and applies a CSP to it, and a sandbox injecting anything into<head>has nowhere to put it when the element is implied.- Nothing is fetched. No CDN, no module import, no external stylesheet --
which is the extension's own advice, and means a view needs no
resource_domainsin its CSP just to boot. - The bridge always completes its handshake, so the frame is always
revealed and a broken view can say what is wrong -- to the console
always, and into the document when
diagnosticsis on. Seebridge.js.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
body
|
str
|
The view's markup. Inserted verbatim -- it is HTML by definition, and it is the project's own template output, not caller input. |
required |
title
|
str
|
The document title. Escaped. |
required |
diagnostics
|
bool | None
|
Whether a protocol failure is written into the document as
well as logged to the console. The text is written for whoever wrote the view, and the audience of
a rendered view is whoever is using the product -- so it is off
where that audience is real. A failed handshake is not necessarily
a failed view either: a host that errors on |
None
|
Returns:
| Type | Description |
|---|---|
str
|
A complete |
Bulk registration¶
register_tools(server, definitions, *, selector_defaults=None, service_defaults=None)
is an additive entry point for registering many tools in one call. Pass
a list of ToolDefinition.service(...) / ToolDefinition.selector(...)
instances plus per-kind ServiceDefaults / SelectorDefaults that fill
in fields each definition leaves as None. Returns the resulting
bindings in input order.
register_tools ¶
register_tools(
server: MCPServer,
definitions: Iterable[ToolDefinition],
*,
selector_defaults: SelectorDefaults | None = None,
service_defaults: ServiceDefaults | None = None,
) -> list[ToolBinding | SelectorToolBinding]
Register every
ToolDefinition
against server.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
server
|
MCPServer
|
The server to register against. |
required |
definitions
|
Iterable[ToolDefinition]
|
The definitions to register, in order. |
required |
selector_defaults
|
SelectorDefaults | None
|
Per-kind defaults merged underneath each selector
definition's own values. Any field the definition sets to something
other than |
None
|
service_defaults
|
ServiceDefaults | None
|
The same, for service definitions. |
None
|
Returns:
| Type | Description |
|---|---|
list[ToolBinding | SelectorToolBinding]
|
The resulting bindings, in the order of |
list[ToolBinding | SelectorToolBinding]
|
harnesses and observability code can introspect what landed. |
Raises:
| Type | Description |
|---|---|
TypeError
|
The definition's |
ToolDefinition
dataclass
¶
Declarative description of a single tool, fed to
register_tools.
A transport-agnostic container for the kwargs that would otherwise be passed to
MCPServer.register_service_tool
or
MCPServer.register_selector_tool,
plus a ToolKind discriminator selecting
between them at dispatch time.
Construct via service / selector, which enforce the
per-kind kwarg surface; direct construction is available for tests and
tooling but bypasses that. Filtering is declared on the spec
(SelectorSpec.filter_set), so neither kind carries a filter_set
kwarg.
Every per-call kwarg defaults to None, which
register_tools reads
as "no override" — letting a
SelectorDefaults
/
ServiceDefaults
supply the value, and falling back to the registration method's own default when
neither does.
display_name
class-attribute
instance-attribute
¶
Consumer-only label, never emitted on the MCP wire. Carried onto the
resulting binding so a downstream library can render a richer label than
the protocol title.
display_description
class-attribute
instance-attribute
¶
Consumer-only blurb, the sibling of display_name and likewise
never emitted on the MCP wire.
always_listed
class-attribute
instance-attribute
¶
Keep this binding in tools/list when
FILTER_LISTINGS_BY_PERMISSIONS would otherwise hide it. None takes
the registration default (False).
spec_kwargs_provides
class-attribute
instance-attribute
¶
Explicit opt-in declaring that spec.kwargs(view, request) supplies
these required callable parameters at dispatch time.
Trust has to be declared per transport, because spec.kwargs is a
runtime callable whose output depends on the view context — URL path params
under DRF, URI template vars for MCP resources, neither for MCP tools.
Supply a sequence to acknowledge that the provider is the static source for
those names.
url_kwargs
class-attribute
instance-attribute
¶
URL-derived values the model supplies as tool args, seeded into the off-HTTP
view.kwargs at dispatch. See
UrlKwarg.
query_params
class-attribute
instance-attribute
¶
Read-shaping values the model supplies as tool args, seeded into the off-HTTP
request.query_params at dispatch. See
QueryParam.
service
classmethod
¶
service(
*,
name: str,
spec: ServiceSpec,
description: str | None = None,
title: str | None = None,
display_name: str | None = None,
display_description: str | None = None,
output_format: OutputFormat | None = None,
permissions: Sequence[Any] | None = None,
rate_limits: Sequence[Any] | None = None,
annotations: dict[str, Any] | None = None,
meta: dict[str, Any] | None = None,
include_structured_content: bool | None = None,
include_output_schema: bool | None = None,
argument_binding: ArgumentBinding | None = None,
unknown_arguments: UnknownArguments | None = None,
always_listed: bool | None = None,
spec_kwargs_provides: Sequence[str] | None = None,
url_kwargs: Sequence[UrlKwarg] | None = None,
query_params: Sequence[QueryParam] | None = None,
) -> ToolDefinition
Typed entry point for service-tool definitions.
selector
classmethod
¶
selector(
*,
name: str,
spec: SelectorSpec,
description: str | None = None,
title: str | None = None,
display_name: str | None = None,
display_description: str | None = None,
input_serializer: type | None = None,
output_format: OutputFormat | None = None,
permissions: Sequence[Any] | None = None,
rate_limits: Sequence[Any] | None = None,
annotations: dict[str, Any] | None = None,
meta: dict[str, Any] | None = None,
paginate: bool | None = None,
include_structured_content: bool | None = None,
include_output_schema: bool | None = None,
argument_binding: ArgumentBinding | None = None,
unknown_arguments: UnknownArguments | None = None,
always_listed: bool | None = None,
spec_kwargs_provides: Sequence[str] | None = None,
url_kwargs: Sequence[UrlKwarg] | None = None,
query_params: Sequence[QueryParam] | None = None,
) -> ToolDefinition
Typed entry point for selector-tool definitions.
The LIST / RETRIEVE shape lives on the spec
(SelectorSpec.kind), not here — the bulk registration loop reads it
from there.
ServiceDefaults
dataclass
¶
Per-kind defaults for
register_tools over
service definitions.
None is the "no override" sentinel: only non-None values are applied as
defaults to the matching
MCPServer.register_service_tool
call, and a per-definition value always wins.
That includes the tri-state fields, where None on the registration
method means "inherit the global setting": passing
include_structured_content=None here is "no override", not a request to
inherit. Leave it unset for the global, or pass True / False.
SelectorDefaults
dataclass
¶
Per-kind defaults for
register_tools over
selector definitions.
Sister of
ServiceDefaults,
with the same convention: None is "no override", deferring to the per-definition
value or to
MCPServer.register_selector_tool's
own default, and a per-definition kwarg always wins on conflict.
The selector-only knobs live here too, so a project wanting every selector
tool to paginate by default says so once. Filtering is not among them —
filter_set is declared on each SelectorSpec, never as a
registration default, and it carries ordering with it.
ToolKind ¶
Bases: Enum
Discriminator for
ToolDefinition
and the
register_tools
dispatch table.
Internal-only — never appears on the wire. SERVICE maps to
MCPServer.register_service_tool,
SELECTOR to
MCPServer.register_selector_tool.
Prefer
ToolDefinition.service
/
ToolDefinition.selector
over passing this kwarg by hand — those are the typed entry points.
ArgumentBinding and UnknownArguments are re-exported from
djangorestframework-services (the transport-neutral dispatch_spec owns
these dispatch policies); import them from rest_framework_mcp.constants.
UnknownArguments ¶
Bases: Enum
How dispatch_spec treats params keys outside a spec's declared set.
The declared set is derived from the spec without any transport knowledge: a
ServiceSpec's
input_serializer fields plus the keys its nested target selectors consume; a
SelectorSpec's
selector parameters. When the set cannot be enumerated — a callable that
declares **kwargs, or a duck-typed filter_set whose fields are opaque to the
core — the spec is treated as open and this policy is a no-op (there is nothing
to call "unknown").
Members (internal knob — the value never appears on a wire):
IGNORE— undeclared keys are dropped (DRF serializers already do this to a mutation's payload, and a selector simply never receives kwargs it doesn't declare). The default, reproducing the pre-policy behaviour.REJECT— an undeclared key raisesValidationError, the same surface a strict serializer produces. Useful when the caller wants a clean correction signal (e.g. a model calling a tool with a mistyped argument).PASSTHROUGH— undeclared keys survive: they are merged onto the mutation'svalidated_databefore the keyword pool is built, so a callable that declares them (or**kwargs) receives them. The one policy that must live insidedispatch_spec— it needs the seam between validation and pool construction that a wrapper cannot reach.
Callers strip their own transport-only keys (pagination, ordering, output format) before calling, so the declared-set check sees only spec inputs.
ArgumentBinding ¶
Bases: Enum
How dispatch_spec turns the flat params into a callable's kwargs.
Every dispatched callable's keyword pool always carries the request /
user seeds (and, for a mutation, data / serializer / instance
/ collection). This enum controls the one remaining question a caller
answers about its wire: can client-supplied input land as individual
keyword arguments, and do those override the spec author's kwargs(...)
invariants? It is a trust-boundary decision, not plumbing — which is why it
belongs to the caller rather than the spec. Pass the member directly; the
value never appears on a wire. Those reserved seeds are always stripped from
the spread in the SPREAD_* modes, so a client cannot poison
transport-controlled state by naming an argument after one of them.
Attributes:
| Name | Type | Description |
|---|---|---|
AUTO |
The default — resolve per spec type: |
|
BUNDLE |
Only the validated payload reaches the callable, as |
|
SPREAD_AUTHOR_WINS |
Client fields are spread into the pool as individual
kwargs, but |
|
SPREAD_CALLER_WINS |
Like |
Chain tools¶
ChainStep is one step of a register_chain_tool sequence — an alias, a
ServiceSpec / SelectorSpec, and an inputs callable. That callable receives
a ChainContext, which exposes the validated tool arguments as ctx.args and
any prior step's output as ctx[alias]. See
Chain specs into one tool.
ChainStep
dataclass
¶
One step in a
ChainToolBinding.
A step wraps a single ServiceSpec (a write) or SelectorSpec (a
read) and binds its output to alias so later steps can read it via
ctx[alias].
What is stored under alias is the final value: for a ServiceSpec
with an output_selector_spec.selector that means the re-fetched value,
so a downstream step reads what the response would serialize.
Attributes:
| Name | Type | Description |
|---|---|---|
alias |
str
|
The name this step's result is stored under in the
|
spec |
ServiceSpec[Any, Any, Any] | SelectorSpec[Any, Any]
|
The |
inputs |
Callable[[ChainContext], Mapping[str, Any]] | None
|
|
ChainContext
dataclass
¶
The accumulating context a chain tool threads through its steps.
Passed to each ChainStep's
inputs callable so a step can build its call kwargs from the validated
tool arguments and any prior step's output:
ctx[alias] is the post-output-selector result a prior step stored, and
raises KeyError for an alias that has not run — only possible when a
step references a later one, a wiring bug worth surfacing loudly. Mutable
by design, and built fresh per tool call, so there is no cross-request
shared state.
Attributes:
| Name | Type | Description |
|---|---|---|
args |
Any
|
The validated chain input — a dataclass instance, a dict, or the raw arguments mapping when no input serializer is resolved. |
request |
Any
|
The synthesised DRF request. |
user |
Any
|
The authenticated user. |
outputs |
dict[str, Any]
|
Alias to step result, filled in as the chain runs. |
ChainToolBinding
dataclass
¶
All wiring for a single MCP tool that runs a sequence of specs.
A chain tool threads a
ChainContext
through its ordered steps — each step's result is stored under its alias and
readable by later steps — so one tool call can express retrieve x → write y →
write z with z derived from both x and y. Sequencing is a transport
concern owned by the MCP layer; the steps themselves are ordinary ServiceSpec /
SelectorSpec units of API behaviour.
Fields not listed below mirror
ToolBinding.
Attributes:
| Name | Type | Description |
|---|---|---|
steps |
tuple[ChainStep, ...]
|
The ordered steps, run front to back. Non-empty, and aliases must be unique. |
input_serializer |
type | None
|
The chain's input schema and validation. |
atomic |
bool
|
Run the whole sequence inside one |
output_alias |
str | None
|
Which step's result is rendered as the tool response.
|
output_all |
bool
|
Render |
display_name
class-attribute
instance-attribute
¶
Consumer-only label, never emitted on the MCP wire, so a downstream
library can render a richer label than the protocol title.
display_description
class-attribute
instance-attribute
¶
Consumer-only blurb, the sibling of display_name and likewise
never emitted on the MCP wire.
icons
class-attribute
instance-attribute
¶
Display icons, emitted in this tool's listing entry. Purely presentational; nothing in dispatch reads them.
content_kind
class-attribute
instance-attribute
¶
What this tool's payload becomes in the result's content array. TEXT
renders JSON per output_format; the other kinds project it into an image / audio
/ resource-link block. See
ToolContentKind.
content_mime_type
class-attribute
instance-attribute
¶
The media type for an IMAGE / AUDIO content_kind.
Required for those and meaningless for the rest — a resource link carries
its own mimeType per entry.
task_policy
class-attribute
instance-attribute
¶
Whether calling this tool hands back a task handle instead of a result.
The choice lives on the binding because the extension makes the server
the sole decider and gives the client no way to ask. See
TaskPolicy.
invalidates
class-attribute
instance-attribute
¶
URI templates naming the resources a successful call changed.
Same contract as invalidates:
published as notifications/resources/updated once the transaction
commits, rendered against the result merged with the call's arguments, and
matched exactly — so name the collection too if you want it watched:
invalidates=("invoices://{pk}", "invoices://")
field_audiences
class-attribute
instance-attribute
¶
Per-tool overrides layered over the FieldMarking declarations the
output serializer carries on its own fields.
The serializer stays authoritative — it is the one declaration the REST API, this transport, and an in-process toolset all read. This exists for the case one tool genuinely needs what a sibling hides: a lookup tool returning the identifier its neighbour drops.
Declared on the registry entry's
OfflineContract
and resolved here, so the field set an agent sees does not depend on which
agent transport served it.
audience_projection
cached
property
¶
This tool's resolved audience markings, derived once per binding.
Drives both the projected payload and the advertised outputSchema,
so the two cannot disagree about which fields a caller will receive.
output_step
property
¶
The step whose result is rendered (output_alias or the last).
resolved_input_serializer
property
¶
The serializer used to validate the chain's arguments.
input_serializer when set, else the first step's
ServiceSpec.input_serializer. Shared by the tools/list schema
builder and the dispatcher, so the advertised schema and the validation
cannot drift.
output_serializer
property
¶
The serializer the rendered output goes through, for outputSchema.
The output step's own (ServiceSpec.output_selector_spec.
output_serializer or SelectorSpec.output_serializer). None
under output_all, where the response is a multi-key object with no
single schema, or when the output step declares no serializer.
Selector-tool schema¶
Builds the merged inputSchema for selector tools — exposed for projects
that want to introspect property generation outside of the registration flow.
The selector's own signature (its declared parameters and an **extras:
Unpack[TypedDict], plus the FilterSet fields) is reflected via
djangorestframework-services'
spec_to_json_schema
— the same reflection the Pydantic-AI toolset consumes — with ordering /
pagination knobs and any explicit input_serializer / UrlKwarg layered on
top, so the shape is described the same way across transports.
build_selector_tool_input_schema ¶
build_selector_tool_input_schema(
binding: SelectorToolBinding, *, max_page_size: int | None = None
) -> dict[str, Any]
Build the JSON Schema for a selector tool's inputSchema.
Merges four sources, in order of precedence (later sources override earlier ones on key collision):
- Reflected
specshape — the selector callable's own parameters (an**extras: Unpack[TypedDict]expanded into one property per key, its required keys populatingrequired, therequest/user/viewtransport seeds skipped) plus thefilter_setfields, via drf-services'spec_to_json_schema. This is the same reflection the Pydantic-AISpecToolsetconsumes, so both transports advertise the same shape: a nested route'sparent_pkread fromextrasis discoverable without an explicitUrlKwarg, and aFilterSet'sOrderingFilteradvertisesorderingwith nothing else declared. spec.input_serializer— tool-specific args that aren't reflected selector params. ASelectorSpeccarries no input serializer, so this is MCP-only; its curated fields win over a reflected param of the same name, and required-marked fields stay required.paginate=True— adds optionalpageandlimitpositive integers.limitcarries amaximumwhenmax_page_sizeis supplied, so the model sees the ceiling dispatch will clamp to.url_kwargs— each registeredUrlKwarg's advertised schema, winning over a reflected key of the same name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
binding
|
SelectorToolBinding
|
The selector tool binding to describe. |
required |
max_page_size
|
int | None
|
The effective page ceiling — the binding's override, else
the server's. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
An object schema carrying |
dict[str, Any]
|
least one required field exists. |
Session stores¶
SessionStore ¶
Bases: Protocol
Pluggable persistence for MCP-Session-Id lifecycle.
The transport calls create after a successful initialize,
binding the new session to the authenticated principal, and owner
on every subsequent request — which is what enforces both that clients
re-initialize after a server restart and that a session minted under one
principal cannot be presented by another. destroy runs on HTTP
DELETE, after the same ownership check.
principal_id is an opaque string the transport derives from the
authenticated token (see
rest_framework_mcp.auth.principal_for_token.principal_for_token);
stores persist and return it verbatim.
InMemorySessionStore ¶
Process-local session store. Useful for tests and single-process dev servers.
State lives on the instance, so each store is isolated. Multi-process deployments
should use
DjangoCacheSessionStore
instead — this class will not see sessions created in another process.
DjangoCacheSessionStore ¶
Session store backed by django.core.cache.
Works across processes — the production-suitable default.
Two windows, both configurable (SESSION_TTL_SECONDS /
SESSION_MAX_AGE_SECONDS, or the constructor arguments here). The TTL is
an idle window that restarts on every successful owner read, so
a session in continuous use never lapses; the max age is the absolute
ceiling that stops a sliding window outliving a revoked principal.
Neither window can promise more than the cache underneath. An eviction
policy like Redis's allkeys-lru drops session keys well before any
timeout, and the client cannot tell that apart from expiry. If sessions
vanish early, check the eviction policy before these settings.
A value this store cannot read fails the ownership comparison and the client transparently re-initializes. A bare principal string — the shape older versions wrote — is honoured and rewritten in the current shape, so an upgrade does not log every current holder out.
Namespacing. An instance built by
MCPServer keys its entries under
the server's name, so two servers in one project cannot see each other's
sessions. Without it they share one flat key space over the same Django cache: a
session minted at one satisfies the other's ownership check, and a DELETE
against either destroys the other's session. The namespace is hashed into the
key (drf-mcp:session:<digest>:<token>) because name is free-form while cache
keys must survive backends like memcached, which reject spaces and control
characters and cap length at 250.
Constructing the store yourself means you own the namespace:
MCPServer(session_store=DjangoCacheSessionStore(namespace="internal"))
exists ¶
Whether a key is present — deliberately not owner() is not None.
A value this store cannot read is present but ownerless, and the gate
keys on owner. Reading through owner here would also
refresh the idle window, making a liveness probe extend what it probes.
owner ¶
Resolve the owning principal, refreshing the idle window on the way.
Idle, not fixed. The window restarts on every successful read, so only a genuinely idle session lapses.
Bounded by an absolute maximum age. The principal binding is
checked once, at initialize, so an unbounded sliding window would
keep a revoked principal alive for as long as it kept talking.
Server-initiated push¶
SSEBroker ¶
Bases: Protocol
Pluggable pub/sub for server-pushed MCP messages.
The transport calls subscribe when a client opens GET /mcp/,
publish from app code pushing a payload to a specific session, and
unsubscribe when the streaming generator unwinds.
Two implementations ship:
InMemorySSEBroker
(single-process, no infra) and
RedisSSEBroker
(the [redis] extra), required for multi-worker deployments where any worker can
serve the streaming GET.
The contract is deliberately narrow: a session has at most one live
subscriber, and publish returns True if a delivery was attempted,
False if no subscriber was attached. Whether publish is
fire-and-forget or awaits confirmation is the implementation's choice; the
transport treats it as best-effort either way.
Implementations must also bound what an undrained subscriber can accumulate. A stream the client stops reading is not an error, and there is no backpressure channel to the publisher, so an unbounded queue turns one paused consumer into unbounded resident memory.
active_streams
property
¶
How many session streams this worker is currently serving.
What MAX_CONCURRENT_SSE_STREAMS is measured against, so it is a
per-worker count of local subscribers rather than a cluster-wide one:
the resource being protected is this process's task pool.
InMemorySSEBroker ¶
In-process per-session pub/sub for server-pushed MCP messages.
Each subscribed session gets a private asyncio.Queue. App code in
the same process publishes to it via publish; the streaming GET
generator pulls off the queue and emits SSE frames.
State is instance-scoped, so multiple servers in one process share none of it.
Multi-process deployments need an out-of-process backend — see
RedisSSEBroker
(the [redis] extra).
One subscriber per session: re-subscribing replaces the previous queue, and
the old generator errors out on its next await. There is no replay;
clients needing durability call tools/call rather than relying on SSE.
Each queue is bounded, and a full one drops its oldest payload. A
client that opens the stream and stops reading it drains nothing, while
notify keeps enqueueing — a paused consumer would otherwise pin one
payload of memory per notification for as long as it holds the connection.
Dropping rather than blocking is the only option that keeps the publisher
honest: publish is called from request handling, so waiting on a reader
that may never return would park the writer too. Dropping is also already
the contract — delivery is best-effort, and a client that missed a
notification re-reads — and the drop is reported, as the False that
publish already uses for "nobody got this".
unsubscribe ¶
Remove queue from the registry if it's still the live subscriber.
Compares by identity so a re-subscribed session doesn't accidentally unregister the new queue when the old generator shuts down.
publish
async
¶
Enqueue payload for session_id if a subscriber exists.
Returns True if delivery was attempted, False if the session
had no subscriber or if the queue was full and the oldest payload was
dropped to make room. A miss is the caller's to react to, and most
ignore it: the client catches up on a fresh tools/call round-trip.
RedisSSEBroker ¶
Cross-process SSE broker backed by Redis pub/sub.
Drop-in replacement for
InMemorySSEBroker
when running multiple ASGI workers behind a load balancer. The streaming GET handler
can land on any worker, and await server.notify(...) from a different worker
still reaches the right session because every worker subscribes to the same Redis
channel (<prefix>:<session_id>). JSON encode/decode happens at the broker
boundary, so app code pushes Python dicts and the streaming generator sees dicts
too.
Wire it into MCPServer:
from redis.asyncio import Redis
from rest_framework_mcp import MCPServer
from rest_framework_mcp.transport.redis_sse_broker import RedisSSEBroker
broker = RedisSSEBroker(Redis.from_url("redis://localhost:6379/0"))
server = MCPServer(name="my-app", sse_broker=broker)
Caveats:
- Same single-subscriber-per-session contract as the in-memory broker: re-subscribing replaces the old subscriber's queue.
- Same bounded per-session queue, for the same reason: the listener task
pumps Redis into a local queue, so a client that stops reading the socket
would otherwise accumulate every published payload in this worker's
memory. Past
max_queued_eventsthe oldest is dropped. - Replay is a separate, opt-in collaborator — pair this with
RedisSSEReplayBufferfor cross-workerLast-Event-IDresume. - The Redis client's lifecycle is the consumer's: close it during ASGI lifespan shutdown.
active_streams
property
¶
Local subscribers only, matching what the cap protects.
The ceiling exists to stop one worker's task pool being exhausted, and a cluster-wide count would neither measure that nor be worth a Redis round-trip per GET.
publish
async
¶
Publish to the session's channel and report whether anyone received it.
True when at least one listener was attached. False — zero
subscribers — can also mean the streaming task has not connected yet,
so a caller needing at-least-once delivery layers its own retry.
has_subscriber ¶
Local-only check: whether this worker has an active subscriber.
Cross-process visibility would cost an extra Redis round-trip and buy nothing — the streaming generator only cares about its own queue.
SSE replay (resume)¶
SSEReplayBuffer ¶
Bases: Protocol
Pluggable per-session ring buffer for SSE event replay.
Pair this with an
SSEBroker to support
Last-Event-ID
resume: when a client reconnects with that header, the SSE response generator drains
every event past the supplied ID before entering live mode, so the client sees no
gap.
The buffer is the single source of truth for event IDs: record
assigns a new monotonic ID per session, so the live frame and any replayed
frame agree. The transport wraps that ID into the broker payload as
{"_mcp_event_id", "_mcp_payload"} and the response generator unwraps it
to emit id: lines.
Implementations must bound their per-session storage — an uncapped buffer leaks when clients never reconnect.
Resume is opt-in: pass sse_replay_buffer=... to
MCPServer. When omitted there
are no id: lines and Last-Event-ID is ignored.
record
async
¶
Persist payload for session_id and return its event ID.
The ID is what the response emits as the id: line and what the
client echoes back via Last-Event-ID. IDs must be monotonic
within a session; cross-session ordering is not required.
replay ¶
Yield (event_id, payload) pairs strictly after after_id.
after_id=None yields nothing — a fresh subscribe is the no-replay
path. An after_id older than the oldest retained event yields
whatever is still held, best-effort: the client can tell it lost events
only by counting. One newer than the latest recorded event yields
nothing, the client being up to date.
forget
async
¶
Drop all retained events for session_id.
Called when a session is explicitly destroyed, so dead sessions do not accumulate buffer state. Implementations relying on TTL eviction can no-op this.
InMemorySSEReplayBuffer ¶
In-process bounded replay buffer for SSE event resume.
Each session holds its own collections.deque capped at
max_events, evicting the oldest event when a new one arrives. Event IDs
are zero-padded monotonic integers per session, string-valued because the
SSE wire format is string-only and clients echo them back verbatim via
Last-Event-ID.
Bounded in both directions. max_events caps one session's history;
max_sessions caps how many sessions are retained at all, dropping the
least recently written when a new one arrives. The second bound is what
stops the buffer growing for the life of the process: forget is called
only when a client explicitly DELETEs its session, while sessions
ordinarily end by expiring in the session store — which notifies nothing —
or by a client simply dropping the connection. Without the cap every
session that ever recorded an event keeps its ring and its counter forever.
An evicted session that is somehow still live restarts its numbering, and a
reconnect carrying the old Last-Event-ID then replays nothing rather
than replaying the wrong events. That is the same silent gap a client
already accepts when its ring overflows, which is why the default is
generous enough that reaching it means far more open sessions than one
worker can serve streams for.
Single-process ASGI deployments only: a resume can land on a different worker
than the one that recorded the events, so multi-worker deployments need
RedisSSEReplayBuffer.
State is instance-scoped, so multiple servers in one process share no replay
history.
RedisSSEReplayBuffer ¶
Cross-process replay buffer backed by Redis Streams.
Drop-in replacement for
InMemorySSEReplayBuffer
when running multiple ASGI workers: a reconnect can land on any worker, and a shared
Redis Stream replays the same events whichever worker recorded them.
Stream IDs are auto-assigned by Redis and monotonic within a session, so
they double as the SSE event IDs the client echoes back via
Last-Event-ID. MAXLEN ~ N caps the retained history per session,
approximately — Redis trims when convenient, which is fine here.
Wire it into MCPServer:
from redis.asyncio import Redis
from rest_framework_mcp import MCPServer
from rest_framework_mcp.transport.redis_sse_replay_buffer import (
RedisSSEReplayBuffer,
)
client = Redis.from_url("redis://localhost:6379/0")
buffer = RedisSSEReplayBuffer(client, max_events=2048, namespace="my-app")
server = MCPServer(name="my-app", sse_broker=..., sse_replay_buffer=buffer)
Every stream carries a TTL. forget runs only on an explicit
DELETE, while sessions ordinarily end by expiring or by a client simply
dropping the connection, so without an expiry each such session leaves its
stream in Redis for good. ttl_seconds is refreshed on every write and
defaults to a day, matching the session store's idle window: past it the
session the stream belongs to could not reconnect anyway.
Pass namespace when one Redis serves more than one server, as the
cache-backed stores do with the server's name. Keys here are addressed
by session id, so a collision needs an id minted by the other server, but
the separation keeps a shared Redis inspectable and matches the
subscription broker, where topics genuinely do collide.
The Redis client is the consumer's responsibility — close it during ASGI lifespan shutdown.
record
async
¶
Append payload to the session's stream and return the assigned ID.
XADD <key> MAXLEN ~ N * data <json>: the * lets Redis choose a
monotonic ID, and ~ trims at internal node boundaries, which bounds
memory in the same shape as exact trimming and is faster.