16 Sep 2026
Django community aggregator: Community blog posts
Django: serve the change password well-known URL
When a password manager detects that a user's password has been leaked or reused, it can prompt them to change it, but the password change URL varies by site. The web's answer to such discovery problems is the reserved /.well-known/ URL namespace (RFC 8615), home to machine-readable files and endpoints like security.txt. A Well-Known URL for Changing Passwords is the web specification that uses this namespace to fix password page discovery. It reserves the URL path /.well-known/change-password to redirect to your actual change password page, wherever that lives.
Password managers that use this URL include Apple's iCloud Keychain (in Safari since 2019), Google Password Manager (since Chrome 86, 2020), and 1Password. web.dev has an excellent article explaining the specification and showing the Google Password Manager feature in action.
In this post, we'll look at implementing the change password URL in a Django project.
Add the redirect
The specification asks that /.well-known/change-password redirect to your change password page with a temporary redirect status code, for which you can use Django's RedirectView. The pattern_name argument looks up the target URL by name, so the redirect stays correct even if you move the page.
So, to add a redirect, plop this path in your root URLconf:
from django.urls import path
from django.views.generic import RedirectView
urlpatterns = [
# ...
path(
".well-known/change-password",
RedirectView.as_view(pattern_name="password_change"),
),
# ...
]
Note the path has no trailing slash, per the specification and counter to Django's default pattern. There's also no need for a name= argument, since nothing on your site should link to the URL.
password_change is the URL name provided by django.contrib.auth.urls, which serves Django's built-in PasswordChangeView. This assumes your URLconf includes those URLs, conventionally mounted at accounts/:
path("accounts/", include("django.contrib.auth.urls")),
If you serve your change password page some other way, swap in the appropriate URL name for pattern_name, such as account_change_password if you use django-allauth. Django doesn't check pattern_name until a request arrives, so a wrong name here fails only when the URL is visited, hence the test below.
Check with runserver and visit http://localhost:8000/.well-known/change-password - you should land on your change password page (or the login page redirecting you there with ?next).
Password managers only ever visit the URL on your live site (with HTTPS), so after deploying, repeat the check on your production domain.
For an end-to-end check, Chrome's password checkup tool (under Settings → Passwords) shows a "Change password" button for compromised entries, which should open your page directly once the redirect is deployed. To make the button appear without waiting for a real breach, temporarily change your saved password for the site to a deliberately weak one, like password123, which Chrome flags in the check.
Public users only
By the way, this feature is for public users and their password change pages, not admins. So don't use this feature to redirect to your Django admin's password change view, or any other private page. That would advertise those URLs to the world, undoing the common hardening of hosting Django's admin at a non-default path.
Check the resource that should not exist
There's a second URL to be aware of, a check for whether your server is broken. Some misconfigured servers respond with 200 to every request, serving an error page instead of using a proper 404 status code. On such a server, a client fetching /.well-known/change-password can't tell whether it found a real change password page or an error page.
The specification solves this with a second reserved path, gloriously named:
/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200
Clients may request this URL to detect broken servers. If it responds with a 200, the server's status codes are deemed meaningless, so the client ignores the change password URL and falls back to something cruder, like opening your homepage.
Django responds with a 404 for unmatched URLs, so your site should pass this check, with nothing to implement. But a catch-all URL pattern could break it, such as one serving pages from a CMS, or a single-page application fallback that serves index.html with a 200 for any path. So it's worth covering with a test, included below.
Add tests
As ever, it's best to include tests to guard against accidental breakage, such as removal of the URL. Here's a test case covering both URLs:
from http import HTTPStatus
from django.test import SimpleTestCase
from django.urls import resolve
class ChangePasswordWellKnownTests(SimpleTestCase):
"""
Test the well-known URLs for changing passwords, per:
https://adamj.eu/tech/2026/09/16/django-change-password-url/
"""
def test_change_password(self):
response = self.client.get("/.well-known/change-password")
self.assertRedirects(
response,
"/accounts/password_change/",
fetch_redirect_response=False,
)
resolve(response["Location"]) # Check it's a real URL
def test_resource_that_should_not_exist(self):
response = self.client.get(
"/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200"
)
self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND)
Notes:
- Neither view uses the database, so the test case uses
SimpleTestCase, which blocks database access and runs a little faster. assertRedirectschecks both the status code, 302 by default, and the target URL.- The target URL is hardcoded, matching where the auth URLs were mounted earlier. If yours live elsewhere, adjust it. Hardcoding, rather than using
reverse(), makes the test check what clients see, rather than using any internal details of your system. - Passing
fetch_redirect_response=Falsestops the test client from following the redirect. Without it,assertRedirectswould fetch the change password page and fail, because when logged out that page responds with a second redirect, to the login page. resolve()raisesResolver404if the target URL doesn't map to a view. Calling it makes up for the skipped fetch above, checking that the redirect points at a real page rather than a typo.
Check your form's autocomplete attributes
The web.dev article also recommends annotating your change password form fields with autocomplete attributes, so password managers can fill in the current password and suggest a generated replacement:
autocomplete="current-password"on the current password fieldautocomplete="new-password"on the new password field(s)
If you use Django's built-in PasswordChangeForm, it's done for you, as the widgets there have included these attributes since Django 3.0.
But if you've built a custom form, it's worth checking that its fields carry the right attributes. You can set them through the attrs argument of each field's widget, for example:
from django import forms
class ChangePasswordForm(forms.Form):
current_password = forms.CharField(
widget=forms.PasswordInput(attrs={"autocomplete": "current-password"}),
)
new_password = forms.CharField(
widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
)
...
If you're customizing Django's flow, prefer subclassing PasswordChangeForm, which carries those attributes already.
Fin
So there we go, a nice little standard to make your user's security a little easier. Add one URL entry and password managers can shepherd your users away from compromised passwords.
May your data never leak and your users passwords always be strong,
-Adam
16 Sep 2026 4:00am GMT
15 Sep 2026
Django community aggregator: Community blog posts
Duff's device, part 2: copying within an array
Duff's device in JavaScript raced hand-written loops that copy one array into another. A reader asked the follow-up: how do they compare with Array#copyWithin, the built-in that copies a range inside a single array? That is a different workload, so it needs its own measurement. The short answer: on every Node and Deno we can install today, the built-in runs 47 to 84 times slower than the loop.

15 Sep 2026 10:00am GMT
Django: introducing django-mcpz, for making MCP servers
I made another package! Say hello to django-mcpz, for building Model Context Protocol (MCP) servers in your Django project.
The package tagline is easy peasy MCP servers in Django.
MCP, the place to be?
MCP specifies a way for LLMs to interact with external data sources. MCP servers can expose a set of tools, which are essentially functions that an LLM can call on behalf of a user. So, for example, a user can ask "where's my pizza at?" and the LLM can call one or more tools on your pizza shop server to navigate your data and return the answer. Users can ask questions in natural language, and LLMs can interpret messy data in your system to return a clean, hopefully-correct answer.
MCP is date-versioned, and the latest version, 2026-07-28, made the protocol much simpler by making it stateless, like ye olde HTTP APIs. There's no longer an initialization handshake, no session ID, and no streaming event stream. Each request is a plain HTTP POST with a JSON body that gets one JSON response, like any other HTTP API.
This new version is way easier to deploy for a typical synchronous, WSGI Django project. The previous default transport required the server to hold open streaming responses (server-sent events) and track sessions, which would entail a separate ASGI deployment using Channels. Now, you can make an MCP server within a single synchronous Django view and keep it inside your normal WSGI deployment, no extra infrastructure required.
One of my clients wants to deploy an MCP server for their app, and so I took it upon myself to take advantage of this new MCP version and build a ground-up implementation, rather than use the existing ASGI-based packages. The goal was to make it "easy peasy" to build an MCP server in your Django project, and so I named it django-mcpz ("pz" read in the American way is "pea-zee", as in "easy peasy") (or maybe I should stick to calling it "pea-zed"?).
django-mcpz targets the latest MCP version, 2026-07-28, with its stateless-by-default transport, but it still works with last year's 2025 versions too, which also had a stateless mode. Client support seems widespread, and anyway, this is an ecosystem that moves fast.
The basics
Here's the example from the README, a server for a shop with one tool that counts orders:
from typing import Literal
import msgspec
from django_mcpz.server import MCPServer
from django_mcpz.bearer_tokens.auth import token_auth
from example.models import Order
server = MCPServer(
name="shop",
version="1.0.0",
instructions="Query the shop's order database.",
auth=token_auth,
)
class CountOrdersParams(msgspec.Struct):
status: Literal["pending", "shipped", "cancelled"] | None = None
class CountOrdersResult(msgspec.Struct):
count: int
@server.tool(
description="Count Order rows, optionally filtered by status.",
read_only=True,
)
def count_orders(request, params: CountOrdersParams) -> CountOrdersResult:
qs = Order.objects.all()
if params.status is not None:
qs = qs.filter(status=params.status)
return CountOrdersResult(count=qs.count())
Some notes:
MCPServerrepresents one server and its registry of tools. It works a bit like Django'sadmin.site: you create one, register things on it, and route it like a plain old view function. Theinstructionsare natural-language guidance for the LLM on how to use the server.- The
authargument is required, since an MCP server exposes your application's internals to network callers. More on that below. server.tool()registers a plain function as a tool. The function receives theHttpRequestfirst, then its parameters, and Django's request/response cycle applies as usual, so it can use the ORM,request.user, and anything else a view can.- Parameters and results are declared as msgspec
Structclasses. django-mcpz generates the tool's JSON Schemas from the type annotations, and validates each call's arguments before the tool runs. Mistyped or unknown arguments are rejected with in-band errors that the calling LLM can read and correct from. msgspec also handles all the JSON serialization and deserialization, via my recently-released django-msgspec, so it's fast. read_only=Truesets one of the specification's tool annotations, hints for client user interfaces about what a tool does. For example, an UI might not prompt for confirmation before calling a read-only tool, but it will for a write tool.
The server is its own view function, so you route it directly in your urls.py:
from django.urls import path
from example.mcp import server
urlpatterns = [
path("mcp", server),
]
And that's all! Deploy as normal, and your MCP server is live at /mcp.
My editor, Zed, has MCP support. I hooked it up to a test project running the above server code and had this brief conversation with an LLM:
## User
count the number of orders in my shop
## Assistant
<thinking>
This "shop" order counting tool has nothing to do with the actual blog project, so it looks like an unrelated test scenario. I'll just go ahead and use the tool as requested.
</thinking>
**Tool Call: Run MCP tool `count_orders`**
Status: Completed
{"count":42}
## Assistant
Your shop has **42 orders** in total.
The LLM detected the "shop" as unrelated to my blog and guessed it was just a test. Well played.
Everything else talking MCP should "just work" too, including Claude Code and Codex.
Authentication
Every server needs an auth callable, which runs before the request body is touched. It receives the request and returns None to allow it, or an HttpResponse to reject it. There's a public callable for servers protected some other way, or that are genuinely public, and you can write your own for, say, an authenticating reverse proxy. But the two common cases are covered by optional apps within django-mcpz.
Developer tools, like Claude Code, Codex, and Cursor, accept a pasted credential in their configuration. For them, there's the django_mcpz.bearer_tokens app, which provides per-client bearer tokens, each acting as a user, and revocable one at a time. Add it to INSTALLED_APPS, run migrate, and pass its token_auth callable to your server, as in the example above. Then create tokens with a management command, which prints the token value once:
$ python manage.py mcpz bearer-tokens create "Claude Code" --user alice
...
…or in the admin. Only a hash of each token is stored, like Django does for passwords, so a leaked database dump does not reveal usable credentials.
Hosted assistants, like Claude.ai and ChatGPT, offer no way to enter a header when adding a server. They connect through OAuth: the user clicks "connect", logs in to your site, approves access, and the assistant receives tokens to call your server with. For these assistants, there's the django_mcpz.oauth app, an authorization server built into your project, implementing the MCP authorization specification and the OAuth standards it draws on. Add the app, include its URLs, and pass its oauth_auth callable to your server:
from django.urls import include, path
from example.mcp import server
urlpatterns = [
path("mcp", server),
path("oauth/", include("django_mcpz.oauth.urls")),
path("", include("django_mcpz.oauth.wellknown")),
]
The app reads everything it needs from your URLconf and each request, so there's nothing else to configure. It ships with a consent page, rendered from templates you can override to match your site, and admin pages for managing clients and tokens. To serve both kinds of client from one server, combine the two callables in a few lines, as covered in the docs.
MCPizza
The django-mcpz repository contains an example project that serves a local MCP server for a pizza place called MCPizza (not to be confused with McPizza). It has tools to check today's date, search the menu, place an order, chart today's orders as an image, and link to the menu web page.
The example is intended to show a use case that allows an LLM to make decisions based on freeform text in a database. Each pizza has structured fields the server enforces, such as price and the dates a special runs between, as well as freeform notes that an LLM can act on, such as "Vegan cheese available on request".
Here's an example from the README, using Claude Code to query the server, calling the current_date and search_menu tools:
$ claude --mcp-config mcp.json --strict-mcp-config --allowedTools "mcp__mcpizza__*" \
-p "What vegetarian pizzas could I order tomorrow for under \$12? I'd prefer vegan if they can do it."
For tomorrow (2026-09-04), the vegetarian options under $12 are:
- **Null Pointer** - $6 - plain base, vegan by default (no cheese/toppings)
- **Garlic Bread (Technically a Pizza)** - $5.50 - vegetarian; GF base available, but not noted as vegan-adaptable
- **Margherita of Theseus** - $9.50 - vegetarian, **vegan cheese available on request**
- **The Off-By-One** (mushrooms, olives, red onion) - $10.50 - vegetarian, **vegan cheese available on request**
Since you'd prefer vegan: **Margherita of Theseus** or **The Off-By-One** both work with vegan cheese swapped in, and **Null Pointer** is vegan as-is (though it's just a plain base). Want me to place an order for one of these?
The README also covers querying it with the llm CLI and the official MCP Python SDK. Give it a whirl and inspect the code to learn more!
Future workings
django-mcpz implements MCP Tools (callable functions), since that's what most Django projects need. There are other parts of the protocol that it might gain, depending on demand:
- Elicitations - server-generated questions for users to answer, that can guide your tool to the right data.
- Prompts - text templates to guide LLMs in using your server for particular tasks.
- Resources - API-like data access, for LLMs to query your server's data directly, rather than through tools.
There's no plan at current for django-mcpz to implement streaming events or async support, since the goal for the package is a simpler implementation that fits the typical Django deployment.
Fin
Please try out django-mcpz today and let me know how it goes.
May your MCP be as EZ as 123,
-Adam
15 Sep 2026 4:00am GMT