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