Settings¶
Every key lives in a single REST_FRAMEWORK_MCP dict in your Django settings.
All of them are optional — the defaults below apply when a key is absent.
# settings.py
REST_FRAMEWORK_MCP = {
"ALLOWED_ORIGINS": ["https://app.example.com"],
"REQUIRE_TOOL_PERMISSIONS": True,
}
Reading an unknown key raises KeyError, so a typo surfaces immediately rather
than silently taking a default.
Assigning the dict replaces it; it does not merge
Absent keys fall back to the defaults above, so a partial
REST_FRAMEWORK_MCP is fine. But assigning the dict — including
settings.REST_FRAMEWORK_MCP = {...} in a test, or @override_settings —
replaces whatever was there, so a project-level opt-out silently disappears
inside that scope. Repeat the keys you rely on in those literals.
The same holds one level down: a dict-valued setting such as
SERVER_INFO is taken whole, not deep-merged. Supplying
{"version": "1.0"} drops any name you had configured alongside it (the
package name is used as the fallback), so give the dict every key you want.
These settings are per-project defaults. Every scalar below can be overridden per server, which is what you want as soon as you mount more than one:
Collaborators are not configured here at all — the auth backend, session
store, SSE broker, server identity and resource URL are passed to
MCPServer(...) in urls.py, because a settings dict can't hold a live
object. Three keys that once named a collaborator by dotted path were removed
and now raise ImproperlyConfigured if still present: AUTH_BACKEND,
SESSION_STORE, AUTH_USER_ADAPTER.
Protocol¶
The companion cacheScope is derived, not configured: a listing filtered by
FILTER_LISTINGS_BY_PERMISSIONS is private, an unfiltered one is public, and
a resource body is always private. public licenses a shared proxy to serve
one response across authorization contexts, which is not a preference — getting
it wrong is a cross-tenant disclosure with a cache in front of it.
What public on a catalog does and doesn't say
The spec's criterion for public is that "the response does not contain
user-specific data" — which an unfiltered catalog satisfies exactly: every
caller gets byte-identical output, so the derivation is the spec's own test
rather than a judgement call.
It is worth being clear what follows, though. public is not a statement
that the catalog is non-confidential. It permits a shared intermediary to
serve a stored copy across authorization contexts, and a tool catalog does
describe the server's capability surface. If that surface is something you
would rather not have cached in front of an authenticated endpoint, turn on
FILTER_LISTINGS_BY_PERMISSIONS — the result becomes per-caller and the
derivation follows it to private.
| Key | Default | What it does |
|---|---|---|
PROTOCOL_VERSIONS |
["2026-07-28", "2025-11-25", "2025-06-18"] |
Every revision this server speaks, most-preferred first, across both eras — 2026-07-28 is modern (per-request metadata, no session), the rest are legacy (initialize handshake). server/discover reports the whole list; each era validates against its own half. initialize never offers a modern version whatever heads the list: the handshake does not exist there, so answering with it would hand the client a protocol the transport would refuse on its next request. |
REQUIRE_PROTOCOL_VERSION_HEADER |
True |
Reject a post-initialize request that omits MCP-Protocol-Version (HTTP 400). Set False for clients that never send it. A header that is present but unsupported is rejected either way — downgrading silently there would mask a real version mismatch. |
SERVER_INFO |
{"name": "djangorestframework-mcp-server"} |
Default serverInfo for initialize. Recognised keys: name, version, title, description, websiteUrl, icons (a list of {src, mimeType, sizes, theme} dicts). Prefer per-server identity: MCPServer(name=…, version=…, title=…, website_url=…, icons=…). description is settings-only — the constructor's description= is the initialize instructions string, which is written for the model rather than for a connection list. |
CATALOG_CACHE_TTL_MS |
60000 |
How long a client may cache a catalog result — server/discover plus the four list methods — emitted as ttlMs. 0 means "immediately stale". A catalog is fixed once the process boots, so the honest ceiling is "until the next deploy", which nothing here can know; a minute costs a client one stale minute after a release rather than a stale catalog for the life of its connection. |
RESOURCE_CACHE_TTL_MS |
0 |
The same for resources/read. 0 by default because a resource body is whatever a selector just produced. A genuinely static resource — an interactive view, a rendered document — opts in per binding with cache_ttl_ms=. |
TASK_TTL_MS |
86400000 (24 h) |
How long a created task stays readable, reported to the client as ttlMs. Both a promise and a bound: after it elapses the record may be dropped, and a client still politely polling gets "unknown task" — which looks exactly like work that vanished, hence the generous default. None disables expiry, which is only sound for a store that evicts on its own (the cache-backed one falls back to a week so an un-polled task cannot pin memory). |
TASK_POLL_INTERVAL_MS |
5000 |
Suggested tasks/get cadence, sent as pollIntervalMs. Advisory — clients SHOULD honour it, and a server MAY rate-limit one that polls faster. Worth tuning to how long the work actually takes: too low and every task costs a stream of no-op polls, too high and a finished task sits while its client waits. None omits the hint. |
INPUT_REQUEST_TTL_SECONDS |
600 (10 min) |
How long a requestState handed to a client stays redeemable. Bounds the replay window on the one value in this protocol that leaves the server, passes through the client, and comes back trusted — the spec requires the expiry alongside the principal and originating-request checks, which are not configurable because there is no defensible value other than "enforced". Long enough to read a confirmation dialog and decide; short enough that a token captured from a log is dead before anyone replays it. |
MAX_INPUT_ROUNDS |
5 |
How many times one call may ask the user for something before failing instead of asking again. The spec explicitly allows asking repeatedly, and a service that wants a confirmation and a reason legitimately needs two rounds. What this bounds is the service whose condition the answer never clears — otherwise client and server volley the same question at a user indefinitely. Past the cap the call returns the service's own message as an isError result. |
Transport & security¶
| Key | Default | What it does |
|---|---|---|
ALLOWED_ORIGINS |
[] |
Origin allowlist, enforced on every request (mandatory per the MCP spec). ["*"] allows any origin — local development only. |
MAX_PROGRESS_NOTIFICATIONS |
1000 |
Ceiling on notifications/progress frames emitted for one request. The spec asks both parties to rate-limit progress, and the failure mode is the familiar one: a service reporting per row over a large table turns one call into a flood. Past the cap further reports are dropped — the dispatch is untouched and the final response still arrives. |
MAX_REQUEST_BYTES |
1048576 (1 MiB) |
Maximum accepted request body size. |
SESSIONS_ENABLED |
True |
Whether the server mints and requires an Mcp-Session-Id. False runs the sessionless legacy mode, which is conformant rather than a fallback — but the session id is what addresses a client's SSE channel, so with it gone the GET stream has no address and answers 405. Request/response tool calling is untouched. |
SESSION_TTL_SECONDS |
86400 (24 h) |
How long a session may sit idle before it expires. The window restarts on every successful read, so a session in continuous use never lapses. |
SESSION_MAX_AGE_SECONDS |
604800 (7 days) |
Ceiling on a session's total lifetime regardless of activity. Not optional in spirit, though None disables it: a session's principal binding is checked once, at initialize, so without an absolute cap a sliding idle window keeps a revoked principal alive for as long as it keeps talking. A cache-backed store may also evict earlier under memory pressure, which is indistinguishable from expiry on the client side — if sessions vanish early, check the eviction policy before this setting. |
SUBSCRIPTION_MAX_SECONDS |
3600 (1 h) |
How long one subscriptions/listen stream may stay open before the server closes it gracefully and the client re-subscribes. An authorization control as much as a resource one: a subscription's permissions are checked once, when it opens, so without a cap a principal whose access was revoked keeps receiving change signals for as long as it holds the connection. None disables the cap — and both bounds with it. |
MAX_CONCURRENT_SUBSCRIPTIONS |
100 |
Ceiling on concurrent subscription streams per worker. Each parks an ASGI task for its lifetime, so without a bound an authenticated caller can exhaust the worker pool by opening streams in a loop. Past the cap a subscription is refused with 503 / -32603 rather than queued. None disables. |
SSE_STREAM_MAX_SECONDS |
3600 (1 h) |
How long one GET session stream may stay open before the server closes it gracefully with a : stream closed comment frame. The session stream's counterpart to SUBSCRIPTION_MAX_SECONDS, and an authorization control on the same terms: the caller is authenticated once, when the stream opens, so without a cap a principal whose access was revoked keeps receiving that session's pushes for as long as it holds the connection — and it holds it indefinitely, since the keep-alive is what stops any proxy reaping it. An SSE client reconnects by itself; pair with sse_replay_buffer= for a gapless reconnect. None disables. |
MAX_CONCURRENT_SSE_STREAMS |
100 |
Ceiling on concurrent GET session streams per worker. Each parks an ASGI task for its lifetime and minting sessions is uncapped, so without a bound an authenticated caller opens one stream per session it minted and exhausts the worker pool. Past the cap the GET is refused with 503 / -32603 rather than queued. None disables. |
RESOURCE_URL |
None |
Canonical resource URL this server publishes — RFC 9728 requires it in protected-resource metadata, and it is what audience enforcement compares against when enabled. Setting it rejects nothing on its own; see ENFORCE_AUDIENCE. Only the default for MCPServer(resource_url=…) — RFC 8707 binds a token to a resource, so each server needs its own URL. Two servers sharing one URL means a token minted for one passes the audience check at the other, which is the exact replay the mechanism prevents. Leaving it unset publishes an empty resource plus a _warning. |
ENFORCE_AUDIENCE |
False |
Whether a token whose bound resource doesn't equal RESOURCE_URL is rejected. The MCP 2026-07-28 spec makes this validation a MUST, so treat the default as a floor rather than a recommendation: django-oauth-toolkit 3.4.0+ records the resource on stock AccessToken, and turning this on needs nothing else. It stays off by default despite the [oauth] extra flooring DOT at >=3.4, because a token that records no resource is rejected and only a client sending RFC 8707's resource parameter records one — an obligation MCP clients have and the other clients of your authorization server do not, so defaulting True would 401 them on upgrade. The check is capability-based, not version-based. With a swapped model without the field, or a DOT installed outside this extra and pinned older, use OAUTH2_PROVIDER["ACCESS_TOKEN_MODEL"] carrying a resource field or DjangoOAuthToolkitBackend(audience_getter=…); without a route to the resource the backend raises ImproperlyConfigured at startup rather than 401-ing every request. Leaving it off on a deployment that could enforce raises UnenforcedAudienceWarning at construction. See Authentication. |
Tools & output¶
| Key | Default | What it does |
|---|---|---|
DEFAULT_OUTPUT_FORMAT |
"json" |
Format of a tool result's human-readable content[0] text for tools that don't set one: "json", "toon", or "auto" (TOON for uniform lists, JSON otherwise). structuredContent is always JSON. Per-tool output_format= wins. See Ship TOON for large lists. |
INCLUDE_STRUCTURED_CONTENT |
True |
Emit structuredContent on tool results. |
INCLUDE_OUTPUT_SCHEMA |
True |
Advertise outputSchema on tool definitions. |
INCLUDE_VALIDATION_VALUE |
False |
Include the offending arguments dict under data.value in validation errors. Off by default — that dict can carry PII or secrets, which would then flow back to the client and into its logs. |
PAGE_SIZE |
100 |
Maximum items returned by one listing call (tools/list, resources/list, resources/templates/list, prompts/list). Clients page with the opaque cursor echoed in the response. |
One combination is a spec violation
INCLUDE_STRUCTURED_CONTENT and INCLUDE_OUTPUT_SCHEMA are independent,
but advertising outputSchema while suppressing structuredContent is
forbidden by the spec and raises ImproperlyConfigured at request time. If
you turn INCLUDE_STRUCTURED_CONTENT off, turn INCLUDE_OUTPUT_SCHEMA off
too (or set include_output_schema=False per binding). The other direction
— structuredContent without outputSchema — is allowed.
Outbound bounds¶
The mirror of MAX_REQUEST_BYTES on the way out. All three accept None to
disable, and all three can be overridden per tool at registration
(max_result_bytes=, dispatch_timeout=, max_page_size=) — pass None
there to lift the bound for one deliberately-large or deliberately-slow tool.
See What the package bounds.
| Key | Default | What it does |
|---|---|---|
MAX_RESULT_BYTES |
5242880 (5 MiB) |
Ceiling on one tool result or resource read, measured on the encoded wire payload. A successful tool result carries the payload twice — as structuredContent and as the spec's backwards-compatibility text mirror — so a ceiling counting one copy would be wrong by 2× against the thing that matters, the client's context window. Over the ceiling the call returns an isError result naming the remedy; it is never truncated, because a clipped list reads as complete to the model reasoning from it. |
MAX_PAGE_SIZE |
100 |
Ceiling on how many rows one selector-tool call returns, paged or not. For a paginate=True tool it bounds the model-supplied limit, and clamps page to the last page that exists so an arbitrarily large page number cannot become an arbitrarily large SQL OFFSET. For a paginate=False tool it is applied as a LIMIT before rendering and the call is refused over the ceiling rather than truncated, for the reason REQUIRE_LIST_PAGINATION gives below. None disables the ceiling on both. Advertised as maximum on the generated inputSchema and clamped at dispatch — the schema tells a well-behaved model what to ask for, the clamp is what stops you trusting it. Clamping is safe here because totalPages / hasNext keep a clamped page self-describing. The default matches the page a selector tool serves when the model omits limit entirely, so an unconfigured deployment never advertises a page larger than the one it defaults to. |
DISPATCH_TIMEOUT |
60.0 |
Wall-clock ceiling, in seconds, on one dispatch. ASGI only — a sync WSGI view has no in-process way to bound its own dispatch. It does not reclaim the worker: a thread parked in a database driver's socket read is not interruptible by asyncio cancellation, so the query runs on. What it buys is a terminal protocol event instead of an open request that never resolves. Pair it with a database statement timeout. |
REQUIRE_LIST_PAGINATION |
False |
Registering a LIST selector tool with paginate=False raises ImproperlyConfigured instead of emitting UnboundedListWarning. Such a tool serialises whatever its selector resolves to — the whole table, for a plain Model.objects.all() — and unlike a paginated tool there is no honest way to clamp it, because the result carries no metadata saying rows were dropped. MAX_PAGE_SIZE is the backstop — applied as a LIMIT before the rows are rendered, so an oversized table is refused rather than fetched and measured, which is what MAX_RESULT_BYTES alone could not do. paginate=True is the fix. |
Permissions¶
| Key | Default | What it does |
|---|---|---|
REQUIRE_TOOL_PERMISSIONS |
True |
Registering a tool, resource or prompt with no permissions at all (neither spec.permission_classes nor permissions=[…]) raises ImproperlyConfigured. Set it to False to downgrade that to an UnguardedToolWarning while migrating a large surface. The warning exists because guarding the viewset — or relying on REST_FRAMEWORK's default permission classes — has no effect over MCP: this package bypasses DRF's view-layer pipeline, so a spec that looks guarded over HTTP ships as an unguarded binding. Interactive views (register_ui_resource) are exempt: a view carries no tenant data by construction. See Authentication. |
REQUIRE_TOOL_DESCRIPTIONS |
False |
Registering a tool with no description raises ImproperlyConfigured instead of emitting UndescribedToolWarning. The description is the only thing a model reads to decide whether and how to call a tool, so an empty one ships a tool that cannot be used correctly — and tools/list renders it indistinguishably from a documented one. There is deliberately no docstring fallback: a docstring is written for the next developer, not for a model choosing between tools. See Documenting tools. |
FILTER_LISTINGS_BY_PERMISSIONS |
False |
Drop bindings whose permissions deny the caller from tools/list, resources/list, resources/templates/list and prompts/list. Per-binding always_listed=True opts one back in as a discovery aid. |
OAuth¶
| Key | Default | What it does |
|---|---|---|
DCR_ENABLED |
False |
Default for build_oauth_urlpatterns(dcr_enabled=). RFC 7591 dynamic client registration; False makes /oauth/register/ refuse every request with 403. An open DCR endpoint lets anyone create an OAuth client against your authorization server — one with no owning user, which is why only the authorization_code grant is registerable here. DCR is deprecated by MCP revision 2026-07-28 in favour of Client ID Metadata Documents, with an earliest removal of the first revision released on or after 2027-07-28; prefer OAUTH2_PROVIDER["CIMD_ENABLED"] and turn this on only for clients that cannot use it. See Authentication. |
DCR_INITIAL_ACCESS_TOKEN |
None |
Default for build_oauth_urlpatterns(dcr_initial_access_token=). The RFC 7591 §3 initial access token clients must present as Authorization: Bearer …. None means no token check — anyone who can reach the endpoint can register. |
SIMPLEJWT_ACCESS_COOKIE |
"access" |
Default for SimpleJWTCookieAdapter(cookie_name=) ([jwt] extra) — the cookie it reads access tokens from. Matches djangorestframework-simplejwt's documented AUTH_COOKIE default. |
Observability¶
| Key | Default | What it does |
|---|---|---|
RECORD_SERVICE_EXCEPTIONS |
False |
Record a ServiceError raised from a tool callable on the active OpenTelemetry span before mapping it to a JSON-RPC error. Off by default because services often raise ServiceError for routine business-rule denials, which would flood error pipelines. ServiceValidationError is never recorded — it is client input failure, not a server fault. See Observability. |