v0.8.0
Added
-
Service & selector tool dispatch now routes through drf-services'
dispatch_spec(pinsdjangorestframework-services0.20). The wire handlers
(handle_tools_call/handle_tools_call_async) and the selector pipeline
hand off the spec-execution core — instance resolution,input_serializer
validation, the kwarg pool (per the binding'sargument_binding/
unknown_arguments), the service / selector run, queryset shaping +
filter_set, and the output-selector re-fetch — to the neutral core, keeping
only the transport shell (MCP permissions / rate limits, ordering, pagination,
output format,structuredContent). Two capabilities come for free by routing
through the path that already composes them:- bulk mutations over MCP —
spec.many(list payload), a
collection_selector_spectarget, and list-shaped output; - object-level permissions over MCP —
spec.permission_classesnow run via
theon_target_resolved=enforce_permissionshook, so ahas_object_permission
rule denies over MCP (previously onlyhas_permissionwas checked).
The reproduced dispatch code is deleted: the local kwarg-pool builder
(build_call_pool), the input-validation/instance-resolution helpers, and the
output-selector re-fetch. - bulk mutations over MCP —
-
MCPServer.call_tool(name, arguments, *, user, request=None)— a blessed,
transport-neutral way to invoke a spec-backed tool off the HTTP / JSON-RPC
path, returning the sameToolResultthe wire handlers build. It is built on
the sister repo'sdispatch_spec/render_spec_output/enforce_permissions,
so the spec-execution core — instance resolution, input validation, the service
/ selector run, the output-selector re-fetch, queryset shaping incl.
filter_set, and the retrieve nullability contract — is shared rather than
re-implemented. An in-process consumer (a bridge, a Pydantic-AI toolset, a
management command) calls this instead of reaching into handler internals. It
honours the binding'sargument_binding/unknown_argumentspolicies and the
spec'spermission_classes(object-level checks included, via the
on_target_resolved=enforce_permissionshook); the read-shaped transport extras
(pagination, ordering, a selector binding's MCP-onlyinput_serializer) and the
transport-level MCP permissions / rate limits stay with the wire handlers. Chain
tools are unsupported (they orchestrate several specs). -
Tools auto-advertise MCP
ToolAnnotationshints. Every tool now
carries the standard hints derived from its mutation profile: selector
tools →readOnlyHint: true; service tools →readOnlyHint: false+
destructiveHint: true; chain tools are read-only only when every step
is a selector.destructiveHint/idempotentHintare emitted only for
non-read tools (per the spec). Hints passed at registration via
annotations=override the derived defaults (e.g.
annotations={"destructiveHint": False, "idempotentHint": True}). The
bundle lands onbinding.annotationsand thetools/listpayload, so a
client can gate destructive tools without a hand-set flag.
Changed
ArgumentBindingis re-exported from drf-services, with renamed members
(breaking).dispatch_specowns the argument-binding policy, so MCP
consumes its enum rather than a parallel copy. The members are renamed to the
neutral-core names —DATA_ONLY→BUNDLE,MERGE→SPREAD_AUTHOR_WINS,
REPLACE→SPREAD_CALLER_WINS(andAUTOis now available). Update
register_*_tool(argument_binding=...)call sites accordingly. Defaults are
unchanged in effect (service toolsBUNDLE, selector tools
SPREAD_AUTHOR_WINS); the import path (rest_framework_mcp.constants) and
UnknownArguments(REJECT/PASSTHROUGH/IGNORE) are unchanged.- Selector tools adopt drf-services' selector contract (two visible
changes). Routing selectors throughdispatch_specmeans: (1) validated
args spread to the selector's declared parameters (coerced via the
binding'sinput_serializer) rather than arriving as onedatabundle —
selectors are reads; a selector should declare its params (def list(*, project_id, ...)), notdef list(*, data). (2) A selector configured with
queryset shaping /filter_setmust return aQuerySet; a non-queryset
return now raisesImproperlyConfigured(a loud developer-facing config
error) instead of silently skipping the shaping. - JSON-Schema generation now delegates to drf-services. MCP's
inputSchema/outputSchemageneration — serializer / dataclass / FilterSet
→ JSON Schema, including drf-spectacular@extend_schema_field/
@extend_schema_serializeroverrides and the LIST pagination envelope — now
routes through the sister repo'sserializer_to_json_schema/
output_to_json_schema/filterset_to_json_schema(drf-services 0.19)
instead of MCP-local copies. The emitted schemas are unchanged; the duplicated
converters (schema/utils.py,schema/spectacular_overrides.py, and the
schema/filterset_schema.pyintrospection) are deleted. MCP keeps the
wrappers that stamp its transport-specific concerns (additionalProperties
perunknown_arguments, theorderingenum, andpage/limit). - Retrieve selectors now shape + filter before
.first(). A
kind=RETRIEVEselector tool now applies queryset shaping
(select_related…extend_queryset) andspec.filter_setto its
queryset before materializing the single instance — so a "stats from a
filtered set" retrieve works over MCP, matching the sister repo's
dispatch_spec.filter_seton a retrieve spec is no longer rejected at
registration (ordering / pagination remain collection-only). Internally,
the selector dispatcher now delegates shaping + filtering to
drf-services' blessedapply_queryset_shapingleaf instead of
re-implementing it (the local_apply_filter_set/_apply_spec_shaping
helpers are gone); non-queryset selector returns still pass through
unfiltered. - Selector filtering is declared on the spec. A selector tool now
reads itsFilterSetfromSelectorSpec.filter_set
(djangorestframework-services0.18+) rather than a separate
filter_set=argument at registration. The one declaration drives both
the HTTP transport and MCP, so a project states its filterable shape
once instead of re-passing it per transport.ordering_fields/
paginatestay on the registration call — they are MCP pipeline
mechanics with no spec analogue. Schema generation and dispatch are
unchanged;binding.filter_setnow delegates to the spec. - Dependency range bumped to
djangorestframework-services>=0.20,<0.21.
Removed
filter_set=is no longer accepted byregister_selector_tool,
the@selector_tooldecorator,ToolDefinition.selector, or
SelectorDefaults. Declare it on the spec instead —
SelectorSpec(kind=…, selector=…, filter_set=MyFilterSet). Pre-1.0
hard cut, no deprecation shim (the consumer set is tiny).