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; ordering_fields /
paginate are binding-level MCP mechanics. 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.
The Generic[InputT, ResultT, ExtraT] parameters mirror
ServiceSpec's generics and are purely informational for type
checkers. They default to Any when omitted, so existing call sites
keep working unchanged.
display_name
class-attribute
instance-attribute
¶
Consumer-only label — never emitted on the MCP wire (tools/list
ignores it). Provided so a downstream library can render a richer label
than the protocol title. None means "unset".
display_description
class-attribute
instance-attribute
¶
Consumer-only blurb, the sibling of :attr:display_name — also never
emitted on the MCP wire. Lets a downstream library show more than the
protocol description. None means "unset".
include_structured_content
class-attribute
instance-attribute
¶
Tri-state override for whether this tool's tools/call response
includes a structuredContent field. None (the default) defers to the
INCLUDE_STRUCTURED_CONTENT setting; True / False force the
behaviour regardless of the global.
include_output_schema
class-attribute
instance-attribute
¶
Tri-state override for whether this tool's tools/list entry carries
an outputSchema. None (the default) defers to the
INCLUDE_OUTPUT_SCHEMA setting; True / False force the behaviour
regardless of the global.
The MCP spec forbids advertising outputSchema while suppressing
structuredContent, so include_output_schema=True together with
include_structured_content=False is rejected at construction time.
argument_binding
class-attribute
instance-attribute
¶
How MCP arguments flow into the kwarg pool. Defaults to BUNDLE
for service tools: mutation services typically take a single
input_serializer-validated data payload, so 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) rejects unknown keys with-32602and advertisesadditionalProperties: false— but only when there is aninput_serializerto validate against. A serializer-less binding has no declared field set, soREJECTcan't fire and its schema stays open (additionalProperties: true).PASSTHROUGHadvertisesadditionalProperties: trueand merges unknown keys into the validated payload.IGNOREadvertisesadditionalProperties: trueand silently drops them.
always_listed
class-attribute
instance-attribute
¶
Opt this binding back into listings it would otherwise be filtered out
of. When FILTER_LISTINGS_BY_PERMISSIONS is enabled, a binding is
normally dropped from tools/list if any of its permissions deny the
caller; True keeps it visible — useful as a discovery aid for admin
tools the caller can see but not invoke (tools/call still 403s).
url_kwargs
class-attribute
instance-attribute
¶
URL-derived values the model supplies as tool args, seeded into the
off-HTTP view's kwargs rather than reaching the service as ordinary
params — from there drf-services spreads them into the dispatch pools, so a
scoping spec.kwargs provider reading view.kwargs sees them. See
:class:UrlKwarg. Advertised in the inputSchema and stripped from the
dispatched params.
SelectorToolBinding
dataclass
¶
Bases: Generic[ResultT, ExtraT]
All wiring for a single MCP read-shaped tool, derived from a SelectorSpec.
Mirrors :class:ToolBinding (which wraps a ServiceSpec for
mutations), but the dispatch pipeline is read-shaped. The shape is
chosen by :attr:kind:
kind=LIST runs the full pipeline:
.. code-block:: text
arguments → validate(merged inputSchema) → run_selector
→ FilterSet(data=...).qs (if ``filter_set`` set)
→ order_by(...) (if ``ordering_fields`` set)
→ paginate (if ``paginate=True``)
→ output_serializer(many=True)
→ ToolResult
kind=RETRIEVE skips ordering / pagination but still applies
queryset shaping + spec.filter_set before materializing the
single instance via .first() (so a "stats from a filtered set"
retrieve works, matching the sister repo's dispatch_spec), then
renders output_serializer(many=False). Combining RETRIEVE
with ordering_fields / paginate is rejected at construction
(those knobs only make sense on a collection).
Selectors return raw, unscoped data (a queryset for LIST, a
single instance for RETRIEVE) — the tool layer owns shape
decisions. A LIST binding with none of filter_set /
ordering_fields / paginate set behaves like a plain RPC
read that calls the selector and renders its return value verbatim.
The Generic[InputT, ResultT, ExtraT] parameters mirror
SelectorSpec's generics and are purely informational for type
checkers.
display_name
class-attribute
instance-attribute
¶
Consumer-only label — never emitted on the MCP wire (tools/list
ignores it). Provided so a downstream library can render a richer label
than the protocol title. None means "unset".
display_description
class-attribute
instance-attribute
¶
Consumer-only blurb, the sibling of :attr:display_name — also never
emitted on the MCP wire. Lets a downstream library show more than the
protocol description. None means "unset".
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 / query
separately. MCP has no such split (every tool call is one JSON
arguments dict), so arguments that aren't filter / ordering /
pagination knobs are declared here.
include_structured_content
class-attribute
instance-attribute
¶
Tri-state override for whether this tool's tools/call response
includes a structuredContent field. None (the default) defers to the
INCLUDE_STRUCTURED_CONTENT setting; True / False force the
behaviour regardless of the global.
include_output_schema
class-attribute
instance-attribute
¶
Tri-state override for whether this tool's tools/list entry carries
an outputSchema. None (the default) defers to the
INCLUDE_OUTPUT_SCHEMA setting; True / False force the behaviour
regardless of the global.
The MCP spec forbids advertising outputSchema while suppressing
structuredContent, so include_output_schema=True together with
include_structured_content=False is rejected at construction time.
argument_binding
class-attribute
instance-attribute
¶
How MCP arguments flow into the kwarg pool. Defaults to
SPREAD_AUTHOR_WINS for selector tools: selectors typically declare their
query parameters as individual function arguments
(def list_drafts(*, project_id, page=1, limit=10)), so the MCP layer
spreads the validated / raw arguments across the pool.
unknown_arguments
class-attribute
instance-attribute
¶
How unknown arguments keys are handled relative to the binding's
merged inputSchema (input_serializer fields + filter_set
properties + ordering + pagination). REJECT (default) rejects unknown
keys with -32602; PASSTHROUGH merges them into the validated
payload; IGNORE silently drops them.
always_listed
class-attribute
instance-attribute
¶
Opt this binding back into listings it would otherwise be filtered out
of — same semantics as :attr:ToolBinding.always_listed, applied to
selector tools when FILTER_LISTINGS_BY_PERMISSIONS is enabled.
url_kwargs
class-attribute
instance-attribute
¶
URL-derived values the model supplies as tool args, seeded into the
off-HTTP view's kwargs rather than reaching the selector as ordinary
params — from there drf-services spreads them into the selector / target
pools. See :class:UrlKwarg. Advertised in the inputSchema, exempt from
the unknown-argument check, and stripped from the dispatched params.
kind
property
¶
Shape discriminator — derived from the spec's required kind field.
Sister-repo 0.13+ made kind a required field on
:class:SelectorSpec, so the binding doesn't store an
independent copy — it would only be a chance for the two to
drift. Exposed as a property so the dispatch layer can keep
reading binding.kind unchanged.
filter_set
property
¶
Transport-neutral filtering, sourced from the spec.
Like :attr:kind and :attr:selector, this delegates to the
:class:SelectorSpec rather than storing a copy — the spec is
the single source of truth (SelectorSpec.filter_set,
djangorestframework-services 0.18+). The MCP read pipeline
and inputSchema generation read binding.filter_set
unchanged, 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 an optional dep behind
the [filter] extra — narrowing the type 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 :func:~rest_framework_services.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.
This type is declared here, not in each adapter, on purpose. It is the
same declaration whichever transport carries it, and two independent copies
had already drifted into validating the same declaration against different
reserved-name sets. Adapters import it and pair it with
:func:~rest_framework_services.validate_channel_names.
Reach for a UrlKwarg 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 that reads 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.
name— the argument / view-kwarg key. Must not collide with a reserved transport key; see :func:~rest_framework_services.validate_channel_names.type— the JSON-Schema type advertised to the caller ("string"by default;"integer"/"number"/"boolean"…).description— optional help text shown to the caller.default— optional value seeded when the caller omits the argument; also surfaced as the schemadefault.required— advertise the key in the schema'srequiredlist. Use it for a route capture the spec genuinely cannot run without, so a caller is told up front instead of failing mid-dispatch. Setting bothrequiredand adefaultis contradictory and raises in :func:~rest_framework_services.validate_channel_names.
required here is the registered-declaration counterpart of the
:data:~rest_framework_services.InputRequired marker, which does the same
job for a key the callable's own TypedDict declares. Both end up in the
schema's required; they differ only in where the key is declared.
json_schema ¶
The JSON-Schema property this kwarg contributes to an input schema.
ToolRegistry ¶
Name → tool binding lookup.
Holds both :class:ToolBinding (service tools, mutations) and
:class:SelectorToolBinding (selector tools, reads). Names share a
namespace — duplicates are rejected loudly so a misconfigured project
surfaces the conflict at discovery time rather than silently shadowing
a tool.
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 consulted by resources/read to render the
selector's return value. mime_type advertises the encoding we will
return — usually "application/json".
kwargs_provider mirrors SelectorSpec.kwargs from
djangorestframework-services >= 0.6: when set, the handler invokes it
once per request and merges the returned dict into the kwarg pool. The
provider receives a synthesised
:class:~rest_framework_services.OfflineServiceView (URI-template
variables exposed as view.kwargs, the binding name as
view.action).
The Generic[ResultT] parameter is purely informational — it lets
callers pin the selector's return type for IDE / type-checker help.
Defaults to Any when omitted.
kind
instance-attribute
¶
Required, no default. Pulled out of SelectorSpec.kind by the adapter
so the binding doesn't carry a reference to the whole spec. LIST invokes
the output serializer with many=True; RETRIEVE (the common case for
URI-template resources) invokes it with many=False. Resources have no
post-fetch pipeline, so both kinds are unconditionally accepted.
encoding
class-attribute
instance-attribute
¶
How the selector's value becomes the resources/read body. JSON
(the default) pretty-prints it; TEXT returns it verbatim, which is what
an HTML / Markdown / CSV resource needs. Declared rather than inferred from
mime_type, so advertising a new mime type never silently changes how the
body is encoded.
always_listed
class-attribute
instance-attribute
¶
Opt this resource back into listings it would otherwise be filtered out
of. With FILTER_LISTINGS_BY_PERMISSIONS enabled, a resource is normally
dropped from resources/list (and resources/templates/list for
templates) if any binding permission denies the caller; True keeps it
visible as a discovery aid. Same semantics as
:attr:ToolBinding.always_listed.
ResourceRegistry ¶
URI / URI-template → :class:ResourceBinding lookup.
Concrete resources (no template variables) are matched by exact URI;
templates are matched by regex derived from the template. resolve
returns the binding plus the variable bindings extracted from the URI.
ResourceEncoding ¶
Bases: str, Enum
How a resource's selector return value becomes the text body.
resources/read advertises mimeType from the binding but the body
encoding is a separate decision, so it is declared separately rather than
sniffed from the mime type — sniffing would silently change behaviour for
anyone already advertising a non-JSON type.
JSON: pretty-print the value as JSON. The default, and what every selector-backed data resource wants.TEXT: the value is already the body. Used for HTML, Markdown, CSV, plain text — anything where JSON-encoding would wrap the payload in a quoted string literal instead of returning it. The selector must return astr.
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 either:
- a list of :class:
PromptMessageinstances (full control), or - a list of strings (each becomes a user text message), or
- a single string (becomes one user text message), or
- a coroutine yielding any of the above.
The handler normalises whatever shape the callable returns into the spec's
messages list at dispatch time.
always_listed
class-attribute
instance-attribute
¶
Opt this prompt back into prompts/list when
FILTER_LISTINGS_BY_PERMISSIONS would otherwise hide it — same semantics
as :attr:ToolBinding.always_listed.
PromptRegistry ¶
Name → :class:PromptBinding lookup.
Mirrors :class:ToolRegistry exactly — names are unique, duplicates
raise loudly at registration time.
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 here, at the registration parameter, rather than in the wire types:
_meta itself is an open namespace shared by every extension, so it
stays a free-form dict at the boundary while each extension keeps its own
closed shape on the way in.
csp— origins the view needs; see :class:UICsp.permissions— browser capabilities the view would use. The host decides whether to grant them.domain— a stable identity for the view's origin, letting a host group views from the same publisher (e.g. for a single consent prompt) rather than treating every URI as unrelated.prefers_border— a rendering hint: 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 list of origins ("https://api.example.com"), mapping
onto the corresponding CSP directive:
connect_domains—fetch/XMLHttpRequest/ WebSocket targets.resource_domains— images, stylesheets, scripts, fonts. A view that loads Django{% static %}assets must list the static origin here; a self-contained single-file template needs nothing.frame_domains— origins the view may itself embed in an iframe.base_uri_domains— permitted values for the document's<base>.
A list is as welcome as a tuple — to_dict copies either way, mirroring
ordering_fields on the selector-tool registrations.
Empty lists are omitted from the payload, so declaring nothing declares nothing — it is not the same as declaring "deny all", which is the host's default anyway.
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.
resource_uri— theui://URI of a view registered on this same server withregister_ui_resource. A concrete URI: the spec defines no expansion mechanism, because the host fetches a view once and then pushes each result into it by notification.visibility— who may call the tool. Empty means "unsaid", which hosts read as the ordinary model-callable default. Host-enforced; this server declares it and does not filtertools/liston it.
The view renders from the tool's structuredContent, which is why a
linked tool must emit it — :meth:~rest_framework_mcp.MCPServer.register_service_tool
and friends refuse a link when it is switched off.
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.
MODEL: the agent may call it — ordinary tool behaviour.APP: the view may call it. AnAPP-only tool is a fine-grained operation that exists to serve the view rather than the conversation.
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 :class:ToolDefinition against server.
Defaults dataclasses supply per-kind kwarg defaults that are merged
underneath each definition's own values (definition wins on
conflict — every field on the definition that is not None is
considered "set by the author").
Returns the list of resulting bindings in the same order as
definitions, so test harnesses and observability code can
introspect what landed.
Raises :class:TypeError if a definition's kind is unrecognised
(the discriminator is internal, so this can only happen via direct
:class:ToolDefinition construction with an unsupported value).
ToolDefinition
dataclass
¶
Declarative description of a single tool, fed to :func:register_tools.
ToolDefinition is a transport-agnostic container — it holds the
kwargs that would otherwise be passed to
:meth:MCPServer.register_service_tool or
:meth:MCPServer.register_selector_tool, plus a :class:ToolKind
discriminator that selects between them at dispatch time.
Construct via the classmethods, not the dataclass constructor — the
methods enforce the per-kind kwarg surface (a service definition
can't set ordering_fields / paginate; a selector definition
can't omit input_serializer quietly etc.). Filtering is declared
on the spec (SelectorSpec.filter_set), not here, so neither kind
carries a filter_set kwarg. Direct construction is available for
tests and tooling but bypasses the type-shape guarantees.
Every per-call kwarg defaults to None; downstream
:func:register_tools treats None as "no override", which lets
a :class:SelectorDefaults / :class:ServiceDefaults instance
supply the value, falling back to the registration method's own
default if neither is set.
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 :attr:display_name — also never
emitted on the MCP wire, and likewise carried onto the binding.
always_listed
class-attribute
instance-attribute
¶
Per-binding opt-back-in to tools/list when
FILTER_LISTINGS_BY_PERMISSIONS would otherwise hide this binding.
None means "use the registration default" (False); True /
False force the behaviour.
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.
None means no opt-in; 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 :class:UrlKwarg). None means
"use the registration default" (no URL kwargs).
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,
) -> 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,
ordering_fields: Sequence[str] | 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,
) -> ToolDefinition
Typed entry point for selector-tool definitions.
The selector's LIST / RETRIEVE shape lives on the spec
(SelectorSpec.kind, required in
djangorestframework-services 0.13+), not here — the bulk
registration loop reads it from there.
ServiceDefaults
dataclass
¶
Per-kind defaults for :func:register_tools over service definitions.
Every field is Optional and None is the "no override"
sentinel — only non-None values are applied as defaults to the
matching :meth:MCPServer.register_service_tool call. A per-
definition value always wins over the default.
Because include_structured_content and include_output_schema
are tri-state on the registration method (None = inherit global
setting, True/False = force), the same None-as-sentinel
convention applies here: ServiceDefaults(include_structured_content=None)
is identical to "no override" — if you want to force every binding
to inherit the global, leave it unset; if you want to force
True/False, pass that explicitly.
SelectorDefaults
dataclass
¶
Per-kind defaults for :func:register_tools over selector definitions.
Sister of :class:ServiceDefaults. Same conventions:
- Every field is
Optional. None= "no override; use the per-definition or the :meth:MCPServer.register_selector_tooldefault".- Per-definition kwargs always win on conflict.
Selector-only knobs (input_serializer, ordering_fields,
paginate) live here too so a project that wants every selector
tool to paginate by default can express that in one place. Filtering
is not among them — filter_set is declared on each
SelectorSpec, not as a registration default.
ToolKind ¶
Bases: Enum
Discriminator for :class:ToolDefinition and the
:func:register_tools dispatch table.
Internal-only — never appears on the wire. Members map directly to
the two registration entry points on :class:MCPServer:
SERVICE→ :meth:MCPServer.register_service_toolSELECTOR→ :meth:MCPServer.register_selector_tool
Use :meth:ToolDefinition.service / :meth:ToolDefinition.selector
instead of constructing :class:ToolDefinition with this kwarg
directly — the classmethods 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 :class:ServiceSpec's input_serializer fields plus the keys its nested
target selectors consume; a :class: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 raises :exc:~rest_framework.exceptions.ValidationError, 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.
Members (the value never appears on a wire — pass the member directly):
AUTO— resolve per spec type to reproduce the pre-policy behaviour: :class:ServiceSpec→BUNDLE(a mutation takes its validated payload as onedatabundle), :class:SelectorSpec→SPREAD_AUTHOR_WINS(a read spreads its params so the selector can declare them as parameters). The default.BUNDLE— only the validated payload reaches the callable, asdata=; individual client fields are not spread as kwargs. The safe default for a mutation whosekwargsscopes the write — the client cannot inject arbitrary keyword arguments.SPREAD_AUTHOR_WINS— client fields are spread into the pool as individual kwargs, butspec.kwargs(...)overrides on conflict. An author-scopedtenant_idcannot be reset by the client. (drf-mcp's formerMERGE.)SPREAD_CALLER_WINS— likeSPREAD_AUTHOR_WINSbut the client wins on conflict:spec.kwargs(...)supplies defaults the client may override. (drf-mcp's formerREPLACE.)
Reserved pool seeds (request / user / data / serializer /
instance / collection) 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.
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 :class:~rest_framework_mcp.registry.types.chain_tool_binding.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].
Fields:
alias— the name this step's result is stored under in the :class:ChainContext. Must be unique within the chain.spec— theServiceSpecorSelectorSpecthis step runs.inputs— optional(ctx) -> Mappingcallable returning the kwargs merged into the step's call pool (alongsiderequest/user);resolve_callable_kwargsthen filters them to the callable's signature.None(the default) means the step receives onlyrequest/user/data(the validated chainargs) — handy for a first step whose service takes the chain input asdata. Any step that needs a prior output or a reshaped payload suppliesinputsexplicitly.
The step's result 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.
ChainContext
dataclass
¶
The accumulating context a chain tool threads through its steps.
Passed to each :class:~rest_framework_mcp.registry.types.chain_step.ChainStep's
inputs callable so a step can build its call kwargs from the
validated tool arguments and any prior step's output:
.. code-block:: python
inputs=lambda ctx: {"account_id": ctx["acct"].id, **ctx.args}
args— the validated chain input (a dataclass instance, a dict, or the raw arguments mapping when no input serializer is resolved).ctx[alias]— the (post-output-selector) result a prior step stored under its alias.KeyErrorif the alias hasn't run yet, which can only happen if a step references a later alias — a wiring bug worth surfacing loudly.request/user— the synthesised DRF request and the authenticated user, for steps whoseinputsneed them.
Mutable by design: the dispatcher appends to outputs as each step
completes. A fresh instance is built per tool call, so there is no
cross-request shared state.
ChainToolBinding
dataclass
¶
All wiring for a single MCP tool that runs a sequence of specs.
A chain tool threads a :class:~rest_framework_mcp.registry.types.chain_context.ChainContext
through its ordered steps — each step's result is stored under its
alias and is readable by later steps — so a single tool call can express
retrieve x → write y → write z with z derived from both x and
y. Sequencing/orchestration is a transport concern owned by the MCP
layer; the steps themselves are ordinary ServiceSpec / SelectorSpec
units of API behaviour.
Fields:
steps— the ordered steps, run front to back. Aliases must be unique. Non-empty.input_serializer— the chain's input schema / validation.Nonefalls back to the first step'sServiceSpec.input_serializer(a first selector step has none, so the chain then validates nothing andctx.argsis the raw arguments mapping).atomic— whenTrue(default) the whole step sequence runs inside a singletransaction.atomic(); any step raising rolls back every prior write. Per-stepspec.atomicis subordinate (steps run withatomic=Falseunder the chain transaction).output_alias— which step's result is rendered as the tool response.None(default) renders the last step. Mutually exclusive withoutput_all.output_all— whenTruethe response is{alias: rendered}for every step that declares an output serializer.
The remaining fields mirror :class:~rest_framework_mcp.registry.types.tool_binding.ToolBinding.
display_name
class-attribute
instance-attribute
¶
Consumer-only label — never emitted on the MCP wire (tools/list
ignores it). Provided so a downstream library can render a richer label
than the protocol title. None means "unset".
display_description
class-attribute
instance-attribute
¶
Consumer-only blurb, the sibling of :attr:display_name — also never
emitted on the MCP wire. None means "unset".
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 (the first-step fallback). A first
selector step contributes no serializer — the chain then validates
nothing and ctx.args is the raw arguments mapping. Shared by the
tools/list schema builder and the dispatcher so the advertised
schema and the validation never drift.
output_serializer
property
¶
The serializer the rendered output goes through, for outputSchema.
The output step's serializer (ServiceSpec.output_selector_spec.
output_serializer or SelectorSpec.output_serializer). None
when output_all (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 the JSON Schema for a selector tool's inputSchema.
Merges five 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, the TypedDict's required keys populatingrequired, therequest/user/viewtransport seeds skipped) plus thefilter_setfields — via drf-services' :func:spec_to_json_schema, the same reflection the Pydantic-AISpecToolsetconsumes, so both transports advertise the same shape. This is what makes a URL kwarg a selector reads from itsextras(a nested route'sparent_pk) discoverable over MCP without an explicitUrlKwarg. spec.input_serializer— any explicit input shape declared by the consumer (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 all required-marked fields stay required.ordering_fields— adds anorderingproperty as an enum of"<field>"and"-<field>"values. Optional.paginate=True— adds optionalpage(positive integer) andlimit(positive integer) properties.url_kwargs— each registered :class:UrlKwarg's advertised schema; wins over a reflected key of the same name (it is the intentional, authoritative declaration).
The final schema is always an object with "type": "object",
"properties": {...}, and "required": [...] only when at
least one required field exists.
Session stores¶
SessionStore ¶
Bases: Protocol
Pluggable persistence for MCP-Session-Id lifecycle.
The transport calls :meth:create after a successful initialize —
binding the new session to the authenticated principal — and
:meth:owner on every subsequent request to enforce both that clients
re-initialize after a server restart and that a session minted under
one principal cannot be presented by another. :meth:destroy is
invoked on HTTP DELETE (after the same ownership check).
principal_id is an opaque string the transport derives from the
authenticated token (see
:func:rest_framework_mcp.transport.utils.principal_for_token);
stores persist and return it verbatim.
.. versionchanged:: 0.7
:meth:create takes a required keyword-only principal_id and
:meth:owner joined the protocol. Custom store implementations
must add both; storing the principal alongside the session id is
the only new obligation.
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 :class: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. TTL is fixed
at 24 hours; for stricter pinning, projects can subclass and override
:meth:create.
The cached value is the owning principal id, so :meth:owner is a
single cache read. Sessions written by pre-0.7 versions stored True
instead of a principal — those fail the ownership comparison and the
client transparently re-initializes.
Namespacing. Every instance built by :class:MCPServer keys its cache
entries under the server's name — the spec's programmatic identifier —
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 rather than interpolated raw:
name is consumer-supplied and free-form ("My Invoicing Server"),
while cache keys must survive backends like memcached that reject spaces
and control characters and cap length at 250. Keys are therefore
drf-mcp:session:<digest>:<token>.
Constructing the store yourself opts out of that — you own the namespace::
MCPServer(session_store=DjangoCacheSessionStore(namespace="internal"))
so two hand-built stores with no namespace collide exactly as before.
Server-initiated push¶
SSEBroker ¶
Bases: Protocol
Pluggable pub/sub for server-pushed MCP messages.
The transport calls :meth:subscribe when a client opens
GET /mcp/, :meth:publish from app code that wants to push a
payload to a specific session, and :meth:unsubscribe when the
streaming generator unwinds.
Two concrete implementations ship today:
- :class:
InMemorySSEBroker— single-process, no infra. Suitable for development and single-worker ASGI deployments. - :class:
RedisSSEBroker— Redis pub/sub. Required for multi-worker deployments where any worker can serve the streaming GET. Pulled in via the[redis]optional extra.
The contract is intentionally narrow: a session has at most one live
subscriber; publish returns True if a delivery was attempted,
False if no subscriber was attached. Implementations decide whether
publish is fire-and-forget or awaits delivery confirmation; the MCP
transport treats it as best-effort either way.
InMemorySSEBroker ¶
In-process per-session pub/sub for server-pushed MCP messages.
Each subscribed session gets a private :class:asyncio.Queue. App code
running in the same Python process publishes to it via :meth:publish;
the streaming GET generator pulls off the queue and emits SSE frames.
State is instance-scoped — the :class:MCPServer owns one broker, so
multiple servers in the same process don't share state. Multi-process
deployments need an out-of-process backend; see
:class:RedisSSEBroker (in the [redis] extra) for the production
choice.
The broker enforces a single subscriber per session — if a client
re-subscribes (e.g. after a dropped connection), the previous queue is
replaced and the old generator will eventually error out on its next
await. There is no replay; clients that need durability should call
tools/call directly rather than relying on SSE.
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. The caller decides how to react to a miss — most
callers will ignore it (the client will catch up via a fresh
tools/call round-trip).
RedisSSEBroker ¶
Cross-process SSE broker backed by Redis pub/sub.
Drop-in replacement for :class:InMemorySSEBroker when running
multiple ASGI workers behind a load balancer. The streaming GET handler
can land on any worker; await server.notify(...) from a different
worker reaches the right session because every worker subscribes to the
same Redis channel.
Each session subscribes to its own Redis channel (<prefix>:<session_id>)
and runs a background asyncio.Task that pulls messages off the
Redis pub/sub stream and pushes them onto a local
:class:asyncio.Queue — the same queue shape the SSE response generator
expects. JSON encode/decode happens at the broker boundary so app code
pushes Python dicts and the streaming generator sees them as dicts too.
Wire it into :class:MCPServer:
.. code-block:: python
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).
- Message replay is a separate, opt-in collaborator — pair this with
:class:
RedisSSEReplayBuffer(passed asMCPServer(sse_replay_buffer=...)) for cross-workerLast-Event-IDresume. - The Redis client's lifecycle is the consumer's responsibility — close it during ASGI lifespan shutdown.
publish
async
¶
Publish to the session's channel and report whether anyone received it.
redis.publish returns the number of subscribers that got the
message; we surface True when at least one listener was attached
(typical case), False otherwise. Note that "0 subscribers" can
also mean the streaming task hasn't connected yet — callers that
require strict at-least-once delivery should layer their own retry.
has_subscriber ¶
Local-only check.
Reflects whether this worker has an active subscriber. Across- process visibility would require an extra Redis round-trip and isn't useful for the typical caller (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 :class: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 from the buffer
before entering live mode, so the client sees no gap from the
server's POV.
The buffer is the single source of truth for event IDs:
:meth:record assigns a new monotonic ID per session and returns it,
so the live frame and any future replayed frame agree on the ID. The
transport wraps that ID into the broker payload as
{"_mcp_event_id", "_mcp_payload"} and the SSE response generator
unwraps it to emit id: lines.
Implementations should bound their per-session storage — replay
buffers without a cap leak when clients never reconnect. The shipped
in-memory variant uses a fixed-size :class:collections.deque; the
Redis variant uses XADD MAXLEN ~ N for capped streams.
Resume is opt-in: pass sse_replay_buffer=... to
:class:MCPServer to enable it. When omitted, the SSE wire shape is
unchanged (no id: lines) and Last-Event-ID from clients is
silently ignored.
record
async
¶
Persist payload for session_id and return its event ID.
The returned ID is what the SSE response emits as the id: line
and what the client echoes back via Last-Event-ID on resume.
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 (no header sent) yields nothing — fresh subscribe
is the no-replay path. An after_id that's older than the
buffer's oldest retained event yields whatever is still in the
ring (best-effort delivery; the client knows it lost some events
only by counting). An after_id newer than the latest recorded
event yields nothing — the client is already up to date.
forget
async
¶
Drop all retained events for session_id.
Called when a session is explicitly destroyed (DELETE) so dead sessions don't accumulate buffer state. Implementations that rely on TTL-based eviction can no-op this.
InMemorySSEReplayBuffer ¶
In-process bounded replay buffer for SSE event resume.
Each session holds its own :class:collections.deque capped at
max_events; the oldest event is evicted 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.
Suitable for single-process ASGI deployments. Multi-worker
deployments must use :class:RedisSSEReplayBuffer because the
streaming GET that handles a resume can land on a different worker
than the one that recorded the events.
State is instance-scoped — :class:MCPServer owns one buffer, so
multiple servers in the same process don't share replay history.
RedisSSEReplayBuffer ¶
Cross-process replay buffer backed by Redis Streams.
Drop-in replacement for :class:InMemorySSEReplayBuffer when running
multiple ASGI workers. The streaming GET that handles a reconnect
can land on any worker; reading from a shared Redis Stream means the
replay is the same regardless of which worker recorded the events.
Stream IDs are auto-assigned by Redis (ms-seq format) and are
monotonic within a session — they double as the SSE event IDs the
client echoes back via Last-Event-ID. MAXLEN ~ N caps the
retained history per session; the ~ makes trimming approximate
(Redis trims when convenient) which is fine for replay buffers.
Wire it into :class: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)
server = MCPServer(name="my-app", sse_broker=..., sse_replay_buffer=buffer)
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; ~ makes trimming approximate (Redis
trims at internal node boundaries, which is faster than exact
trimming and bounds memory in the same shape).