Django: introducing django-mcpz, for making MCP servers

MCP servers, come make your 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:

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:

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


Read my book Boost Your Django DX, freshly updated in November 2024.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: