Skip to content

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 20 May 19:33
· 29 commits to main since this release
db883e6

Changed

  • Structural cleanup (Phase 10h) — pure refactor, no behavior
    change. Public top-level rest_framework_mcp re-exports unchanged.

    • types/ sub-packages. Every parent package that mixed type
      declarations with functionality now has a types/ sibling that
      holds the dataclasses and Protocols. Affected packages:
      registry/, protocol/, auth/, auth/permissions/,
      auth/rate_limits/, transport/, handlers/, server/,
      contrib/oauth/, contrib/oauth/adapters/. Internal imports
      point at the new leaf paths; package __init__.py re-exports
      preserve the existing public surface.
    • dict[str, Any] → dataclasses for the OAuth/auth metadata
      payloads. New ProtectedResourceMetadata,
      AuthorizationServerMetadata, OpenIDDiscoveryPayload,
      DynamicClientRegistrationRequest,
      DynamicClientRegistrationResponse (under auth/types/ and
      contrib/oauth/types/). MCPAuthBackend Protocol signatures
      updated; both shipped backends and the OAuth views serialize via
      .to_dict().
    • DynamicClientRegistrationSerializerDataclassSerializer
      over the new request dataclass. The DCR ViewSet consumes the
      typed instance via serializer.save().
    • View / APIViewViewSet for every package-owned HTTP
      endpoint. Files + classes renamed:
      ProtectedResourceMetadataView → ProtectedResourceMetadataViewSet,
      AuthorizationServerMetadataView → AuthorizationServerMetadataViewSet,
      OpenIDDiscoveryView → OpenIDDiscoveryViewSet,
      DynamicClientRegistrationView → DynamicClientRegistrationViewSet,
      StreamableHttpView → StreamableHttpViewSet,
      AsyncStreamableHttpView → AsyncStreamableHttpViewSet. Each
      mounts via .as_view({method: action}, ...); canonical action
      maps STREAMABLE_HTTP_ACTION_MAP and
      ASYNC_STREAMABLE_HTTP_ACTION_MAP re-exported from the
      ViewSet modules for terse URL conf. The async transport
      additionally overrides as_view/dispatch so Django routes
      the coroutine-returning view correctly (DRF's ViewSet dispatch
      is still sync-only as of 3.17). AuthorizePassthroughView
      stays as a DOT AuthorizationView subclass — documented
      exception, since the parent class lives in DOT.
  • Bumped the djangorestframework-services pin from ==0.11.0 to
    ==0.12.0. The MCP layer now honors three sister-repo additions
    automatically — no migration steps for existing tools / resources, but
    spec authors can now lean on them instead of duplicating the same
    configuration at the MCP registration call:

    • spec.permission_classes (DRF BasePermission classes) on both
      ServiceSpec and SelectorSpec (and on the SelectorSpec used for
      resources) is wrapped via the new DRFPermissionAdapter and
      prepended to the per-binding permissions tuple. Spec-declared
      permissions run first; tool-level MCPPermission instances run
      after. Misconfigurations (instances instead of classes,
      non-BasePermission subclasses) raise TypeError at registration
      time. The same spec that backs an HTTP view now governs the MCP
      binding without restating the permission contract.
    • Per-spec QuerySet shaping (select_related, prefetch_related,
      annotations, extend_queryset) on SelectorSpec is applied to
      the queryset returned by the selector before the FilterSet / ordering
      / pagination pipeline. extend_queryset runs last so it always sees
      the fully statically-shaped queryset, matching sister-repo's
      ordering. Non-queryset returns (lists, scalars) pass through
      shaping unchanged.
    • Per-spec serializer context (input_serializer_context and
      output_serializer_context on ServiceSpec,
      output_serializer_context on SelectorSpec) is invoked with the
      synthesised view + DRF request and forwarded as context= into the
      serializer constructor — both sync and async dispatch paths.

Added

  • New REST_FRAMEWORK_MCP["FILTER_LISTINGS_BY_PERMISSIONS"] setting
    (default False). When enabled, tools/list, resources/list,
    resources/templates/list, and prompts/list drop bindings whose
    permissions deny the current caller before paginating, so
    nextCursor reflects the user-visible slice. Per-binding
    always_listed=True (ToolBinding / SelectorToolBinding /
    ResourceBinding / PromptBinding, plus the matching server
    registration entry points and ToolDefinition) opts a binding back
    into the listing as a discovery aid for admin-only operations where
    the caller should see the name but can't invoke it. Custom
    permissions can override list-time visibility by declaring an
    is_listable(token) method alongside has_permission; the default
    falls back to has_permission(synthetic_request, token) for
    binding-level permissions like ScopeRequired /
    DRFPermissionAdapter. Filter is point-in-time only —
    per-call-argument permissions evaluate against a data-less request at
    list time, so this is binding-level gating, not per-record gating.
  • Conformance test suite (tests/conformance/) — drives every Phase 10
    feature through the live Django URL conf + JSON-RPC transport so the
    wire shape is what an MCP client actually sees. Covers
    ArgumentBinding.MERGE spread + pool-seed protection, all three
    UnknownArguments policies, register_tools bulk-registration
    parity, spec.permission_classes denial through the transport,
    every PRM / AS / OIDC / DCR endpoint in the contrib mount including
    alias-renders-not-redirects, and the SimpleJWT cookie adapter
    hydrating request.user before DOT's AuthorizationView dispatches.
    Suite shares the existing jsonrpc / initialized_session fixtures
    but routes through tests.conformance.urls (mounted via
    per-module pytestmark = pytest.mark.urls(...)).
  • New AuthUserAdapter Protocol (rest_framework_mcp.contrib.oauth.adapters)
    plus a reference SimpleJWTCookieAdapter implementation behind a new
    [jwt] extra. Adapters hydrate request.user before DOT's
    AuthorizationView dispatches — the typical "DRF backend with
    SimpleJWT cookies on the same host" deployment where DOT's view
    doesn't know about the JWT cookie and would otherwise treat the user
    as anonymous. Configure via REST_FRAMEWORK_MCP["AUTH_USER_ADAPTER"]
    (dotted path) plus REST_FRAMEWORK_MCP["SIMPLEJWT_ACCESS_COOKIE"]
    (cookie name, default "access"). Mount the passthrough by passing
    include_authorize=True to build_oauth_urlpatterns(...) — the
    resulting view is a thin DOT AuthorizationView subclass with the
    adapter hook bolted onto dispatch. Without an adapter configured
    it's functionally identical to DOT's view, so the flag is safe to
    enable in every deployment.
  • New rest_framework_mcp.contrib.oauth namespace with
    build_oauth_urlpatterns(server=, include_dcr=, include_aliases=, include_openid_discovery=) plus the underlying views
    (AuthorizationServerMetadataView, OpenIDDiscoveryView,
    DynamicClientRegistrationView, DynamicClientRegistrationSerializer).
    Opt-in glue — the core MCPServer.urls mount stays minimal (PRM
    only). Mount the helper alongside your server URLs to expose the full
    endpoint matrix MCP / LLM-host clients probe (RFC 8414 + RFC 9728 +
    OIDC discovery + RFC 7591 DCR) plus the alias paths different
    clients use. Aliases render the canonical payload — they're not HTTP
    redirects. DCR is gated behind two new settings:
    REST_FRAMEWORK_MCP["DCR_ENABLED"] (default False) and
    REST_FRAMEWORK_MCP["DCR_INITIAL_ACCESS_TOKEN"] (default None).
  • MCPAuthBackend Protocol gained an authorization_server_metadata()
    method. DjangoOAuthToolkitBackend implements it (RFC 8414 payload
    derived from SERVER_INFO). AllowAnyBackend raises
    NotImplementedError — the contrib mount surfaces that as 501 Not Implemented on the AS endpoints so a dev-mode server doesn't have to
    silently serve a fake AS metadata payload.
  • New register_tools(server, definitions, *, selector_defaults, service_defaults)
    bulk-registration entry point plus the supporting ToolDefinition,
    SelectorDefaults, ServiceDefaults dataclasses and ToolKind
    discriminator enum. Additive — the existing imperative and decorator
    registration surfaces are unchanged. ToolDefinition.service(...) /
    ToolDefinition.selector(...) are the typed entry points; passing a
    list of definitions plus per-kind defaults dataclasses collapses
    repetitive registration boilerplate without parallelising the
    dispatch engine (it loops over the existing per-tool methods, so
    every guarantee and bug fix applies automatically). Per-definition
    kwargs win over defaults on conflict; None is the "no override"
    sentinel across both layers. Returns the list of resulting bindings
    in input order so test harnesses can introspect.
  • New UnknownArguments enum and matching unknown_arguments= kwarg
    on register_service_tool, register_selector_tool, and their
    decorator forms. Controls how MCP arguments keys outside the
    binding's declared field set are handled:
    • UnknownArguments.REJECT (default) — outer inputSchema
      advertises "additionalProperties": false and the validator
      rejects unknown keys with -32602 (per-field
      non_field_errors). Selector tools' pipeline-reserved keys
      (ordering / page / limit and filter-set property names) are
      automatically considered "known" so the policy doesn't fight the
      post-fetch pipeline.
    • UnknownArguments.PASSTHROUGH — outer inputSchema advertises
      "additionalProperties": true; unknown keys survive validation
      and are merged onto the validated payload before binding. The
      serializer's coerced values for declared fields still win over
      the raw values. Only effective on plain Serializer outputs;
      bare-dataclass inputs receive IGNORE-equivalent behaviour
      (a frozen dataclass instance isn't a merge target).
    • UnknownArguments.IGNORE — outer inputSchema advertises
      "additionalProperties": true; unknown keys are silently dropped
      after validation (the DRF default). Forward-compat mode.
      Reserved transport-pool seeds (request / user / data) are
      never treated as "unknown" — they're handled by the dispatch pipeline
      and silently dropped from any client spread regardless of policy.
  • New ArgumentBinding enum and matching argument_binding= kwarg on
    register_service_tool, register_selector_tool, and their
    decorator forms. Controls how MCP arguments flow into the kwarg
    pool of the dispatched callable:
    • ArgumentBinding.DATA_ONLY (default for service tools) — historical
      shape, arguments only enter the pool as data=<validated>.
    • ArgumentBinding.MERGE (default for selector tools) — every key
      from the validated arguments (or raw arguments when no validator)
      is added to the pool as a top-level kwarg, so selectors can declare
      individual parameters (def list_drafts(*, project_id, page=1)).
      spec.kwargs(...) overrides on conflict so author-declared
      invariants win over client-supplied values.
    • ArgumentBinding.REPLACE — like MERGE, but the spread wins on
      conflict so spec.kwargs(...) can supply client-overridable defaults.
      Reserved keys (ordering / page / limit from the selector-tool
      post-fetch pipeline; request / user / data from the pool seeds)
      are stripped from the spread in MERGE/REPLACE modes, so clients
      can't poison transport-controlled state. The default for selector
      tools flips from data-only to merge; selectors that were registered
      expecting data=<arguments-dict> continue to receive it (data= is
      still in the pool in every mode) but can now also be declared with
      individual parameters.
  • New DRFPermissionAdapter class (rest_framework_mcp.auth.permissions)
    that bridges a DRF BasePermission class into the MCPPermission
    Protocol. Re-exported from the top-level rest_framework_mcp. Construct
    one directly if you need the same DRF permission gating without going
    through spec.permission_classes (e.g. for tool-level overrides).
  • New REQUIRE_PROTOCOL_VERSION_HEADER setting (default True). Some MCP
    clients omit the MCP-Protocol-Version header entirely on non-initialize
    requests, which the spec-compliant default rejects with HTTP 400. Set this
    to False to accept those requests by falling back to the first entry of
    PROTOCOL_VERSIONS. A present-but-unsupported version is still rejected
    either way — silently downgrading would mask a real version mismatch.
  • New INCLUDE_STRUCTURED_CONTENT setting (default True) plus a matching
    per-tool override include_structured_content on register_service_tool,
    register_selector_tool, and their decorator forms. Controls whether
    tools/call responses include the structuredContent field alongside
    the human-readable content[0] text. Set the global to False (or the
    per-tool override) to omit it for clients that echo both fields back to
    the LLM and burn context, or that choke on the field altogether. The text
    payload still carries the full data, so no information is lost; clients
    just have to re-parse instead of getting a pre-parsed dict. The
    per-binding override is tri-state — None (default) inherits the
    global, True/False force the behavior regardless of the setting.
    When structuredContent is omitted for a binding, its outputSchema is
    also dropped from tools/list — the MCP tools spec requires that a tool
    declaring outputSchema always return conforming structuredContent, so
    the two are kept in lockstep to avoid advertising a contract the server
    then refuses to honor.