Skip to content

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

display_name: str | None = None

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

display_description: str | None = None

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

include_structured_content: bool | None = None

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

include_output_schema: bool | None = None

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

argument_binding: ArgumentBinding = BUNDLE

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

unknown_arguments: UnknownArguments = REJECT

How unknown arguments keys are handled relative to the binding's inputSchema.

  • REJECT (default) rejects unknown keys with -32602 and advertises additionalProperties: false — but only when there is an input_serializer to validate against. A serializer-less binding has no declared field set, so REJECT can't fire and its schema stays open (additionalProperties: true).
  • PASSTHROUGH advertises additionalProperties: true and merges unknown keys into the validated payload.
  • IGNORE advertises additionalProperties: true and silently drops them.

always_listed class-attribute instance-attribute

always_listed: bool = False

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_kwargs: tuple[UrlKwarg, ...] = ()

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

display_name: str | None = None

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

display_description: str | None = None

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

input_serializer: type | None = None

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

include_structured_content: bool | None = None

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

include_output_schema: bool | None = None

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

argument_binding: ArgumentBinding = SPREAD_AUTHOR_WINS

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

unknown_arguments: UnknownArguments = REJECT

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

always_listed: bool = False

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_kwargs: tuple[UrlKwarg, ...] = ()

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

kind: SelectorKind

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

filter_set: Any | None

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 schema default.
  • required — advertise the key in the schema's required list. 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 both required and a default is 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

json_schema() -> dict[str, Any]

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

kind: SelectorKind

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

encoding: ResourceEncoding = JSON

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

always_listed: bool = False

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 a str.

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:PromptMessage instances (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

always_listed: bool = False

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

to_dict() -> dict[str, Any]

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_domainsfetch / 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

to_dict() -> dict[str, Any]

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 — the ui:// URI of a view registered on this same server with register_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 filter tools/list on 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

to_dict() -> dict[str, Any]

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. An APP-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

display_name: str | None = None

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

display_description: str | None = None

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

always_listed: bool | None = None

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

spec_kwargs_provides: Sequence[str] | None = None

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_kwargs: Sequence[UrlKwarg] | None = None

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_tool default".
  • 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_tool
  • SELECTOR → :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's validated_data before the keyword pool is built, so a callable that declares them (or **kwargs) receives them. The one policy that must live inside dispatch_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:ServiceSpecBUNDLE (a mutation takes its validated payload as one data bundle), :class:SelectorSpecSPREAD_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, as data=; individual client fields are not spread as kwargs. The safe default for a mutation whose kwargs scopes the write — the client cannot inject arbitrary keyword arguments.
  • SPREAD_AUTHOR_WINS — client fields are spread into the pool as individual kwargs, but spec.kwargs(...) overrides on conflict. An author-scoped tenant_id cannot be reset by the client. (drf-mcp's former MERGE.)
  • SPREAD_CALLER_WINS — like SPREAD_AUTHOR_WINS but the client wins on conflict: spec.kwargs(...) supplies defaults the client may override. (drf-mcp's former REPLACE.)

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 — the ServiceSpec or SelectorSpec this step runs.
  • inputs — optional (ctx) -> Mapping callable returning the kwargs merged into the step's call pool (alongside request / user); resolve_callable_kwargs then filters them to the callable's signature. None (the default) means the step receives only request / user / data (the validated chain args) — handy for a first step whose service takes the chain input as data. Any step that needs a prior output or a reshaped payload supplies inputs explicitly.

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. KeyError if 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 whose inputs need 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. None falls back to the first step's ServiceSpec.input_serializer (a first selector step has none, so the chain then validates nothing and ctx.args is the raw arguments mapping).
  • atomic — when True (default) the whole step sequence runs inside a single transaction.atomic(); any step raising rolls back every prior write. Per-step spec.atomic is subordinate (steps run with atomic=False under the chain transaction).
  • output_alias — which step's result is rendered as the tool response. None (default) renders the last step. Mutually exclusive with output_all.
  • output_all — when True the 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

display_name: str | None = None

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

display_description: str | None = None

Consumer-only blurb, the sibling of :attr:display_name — also never emitted on the MCP wire. None means "unset".

output_step property

output_step: ChainStep

The step whose result is rendered (output_alias or the last).

resolved_input_serializer property

resolved_input_serializer: type | None

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

output_serializer: type | None

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_selector_tool_input_schema(binding: SelectorToolBinding) -> dict[str, Any]

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):

  1. Reflected spec shape — the selector callable's own parameters (an **extras: Unpack[TypedDict] expanded into one property per key, the TypedDict's required keys populating required, the request / user / view transport seeds skipped) plus the filter_set fields — via drf-services' :func:spec_to_json_schema, the same reflection the Pydantic-AI SpecToolset consumes, so both transports advertise the same shape. This is what makes a URL kwarg a selector reads from its extras (a nested route's parent_pk) discoverable over MCP without an explicit UrlKwarg.
  2. spec.input_serializer — any explicit input shape declared by the consumer (tool-specific args that aren't reflected selector params). A SelectorSpec carries 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.
  3. ordering_fields — adds an ordering property as an enum of "<field>" and "-<field>" values. Optional.
  4. paginate=True — adds optional page (positive integer) and limit (positive integer) properties.
  5. 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

unsubscribe(session_id: str, queue: Queue[Any]) -> None

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

publish(session_id: str, payload: Any) -> bool

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 as MCPServer(sse_replay_buffer=...)) for cross-worker Last-Event-ID resume.
  • The Redis client's lifecycle is the consumer's responsibility — close it during ASGI lifespan shutdown.

publish async

publish(session_id: str, payload: Any) -> bool

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

has_subscriber(session_id: str) -> bool

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

record(session_id: str, payload: Any) -> str

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

replay(session_id: str, after_id: str | None) -> AsyncIterator[tuple[str, Any]]

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

forget(session_id: str) -> None

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

record(session_id: str, payload: Any) -> str

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).