Build1 publisher3 min readPublished
A Django MCP handler reaches the ORM with only a tool name and an arguments dict
A consultancy's account of production MCP work on Django client apps calls the protocol a transport layer. Its sample server calls django.setup(), imports the Order model, and dispatches tool calls on a string.
The Engineer · Build desk

What happened
- A consultancy's write-up of several months building MCP servers for client Django applications lists what the protocol standardises: tools, resources and prompts over JSON-RPC on stdio or HTTP/SSE.
- The update tool's description tells the model that cancelling an order requires a reason, while the Pydantic input model declares reason as optional with a default of None.
- The post calls tool descriptions the most important part of an MCP server and says to treat them as seriously as a system prompt.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- cost Every team shipping this pattern writes and maintains its own auth, rate limiting and audit trail, and pays again each time the web app's rules change and the MCP copy has to follow.
- constraint Permission logic that lives in Django views is unreachable from an MCP call, so the enforceable copy has to sit in the service layer both the web app and the MCP process call.
- exposure Any order ID an agent can produce, including one carried over from another customer's conversation, resolves through the same read handler.
- contradiction The post tells teams to start read-only, and the reference server it publishes ships a mutating tool next to the read one.
Read the handler signature before the pitch. In the sample server the dispatcher is declared `async def call_tool(name: str, arguments: dict) -> list[TextContent]`, and it picks a branch by comparing that name against string literals [12]. No session, no user, no token arrives with the call. Any decision about who may invoke `update_order_status` has to be made before the process starts, or inside the service function the handler calls [1].
The module sets `DJANGO_SETTINGS_MODULE`, calls `django.setup()`, then imports the `Order` model plus `get_order_summary` and `update_order_status` [8]. The write-up says the server is a standalone process that imports Django models and services and is not a Django view [7]. An MCP call bypasses Django's middleware chain, and view-level permission decorators sit outside that path [4].
The read tool runs `Order.objects.select_related("customer").prefetch_related("line_items").get(order_id=params.order_id)` and returns `get_order_summary(order)`; a miss returns the text "Order {id} not found." [13]. The only filter is the order ID the model supplied, so any scoping to one customer has to happen inside `get_order_summary` [3].
The write tool has the same shape. Its description tells the model "Requires a reason when cancelling" [10], while `UpdateOrderStatusInput` declares `reason: str | None = None` [11]. A call with `new_status="cancelled"` and no reason passes Pydantic and reaches the service function [2].
On this the post is blunt: "What it doesn't give you: security, access control, rate limiting, or any business logic. MCP is a transport layer. All of that still needs to be built on top." [5] What it does supply, per the same post, is one way to expose tools, resources and prompts to any compatible client over JSON-RPC on stdio or HTTP/SSE [4]. The sample runs the stdio transport under `__main__` and is started with `python mcp_server/server.py` [14]. Over stdio the exchange happens on the process's own standard input and output, so whoever can start the process is the trust boundary. Put the same handler behind the HTTP/SSE transport the post names and that boundary goes away, and the shown code carries no replacement for it [6].
The advice on descriptions is good craft and worth following. The post contrasts "Get order details" with a description that states the ID format ORD-XXXXX, lists what comes back, and ends "Use this before attempting to update an order status." [15] That precision is aimed at the model choosing the call, and it has no hold on the call once made.
The evidence here is one shop's account of a few months of client work, and it reports no measurements [3]. The transport claim can be checked against the spec, so it holds up outside this one shop. The tradeoff advice is experience: start read-only, add write tools only after working through an incorrect call [16]. The post says exposing a `send_email` tool early on one project was a mistake and begins to describe the context the agent used it in, and the published text breaks off mid-sentence [17].
What to watch
- Whether the mcp Python SDK adds caller identity or a per-tool authorisation hook to the call_tool signature.
- Whether the completed version of the post publishes what the agent actually did with the send_email tool.
- Whether teams adopting this pattern move permission checks down into shared service functions, which would show up as changed signatures on get_order_summary and update_order_status.