Ownership
Scope reads and writes to "rows the authenticated user owns" with a single attribute.
Why
A common mistake on multi-user APIs is letting user A read or mutate user B's data because the URL has a row id and the handler trusts it. The fix is to always filter reads and writes by the owner column. 0-mcp makes this declarative.
Usage
class TodoResource(BaseResource):
model = Todo
owner_field = 'owner_id' # column on Todo that holds the user idOther rows return 404 — indistinguishable from "row does not exist", which is the right answer for an attacker. The same scoping applies to MCP tool calls — an agent authenticated for user A cannot read or mutate user B's row.
🤖 MCP — tools/call
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "delete_todo",
"arguments": {"id": 42}
}
}🌐 REST
GET /todos GET /todos/42 PATCH /todos/42 DELETE /todos/42
Behaviour
| State | Behaviour |
|---|---|
owner_field not set | No filtering — handler operates on the row by id |
owner_field set, no authenticated user | 401 (cannot determine ownership) |
owner_field set, user owns row | Operation proceeds |
owner_field set, user does not own row | 404 |
Reads
owner_field filters both reads and writes. Detail GET, list GET, PATCH and DELETE all respect the owner column automatically:
class TodoResource(BaseResource):
model = Todo
owner_field = 'owner_id'
# No extra queryset_filter needed for ownership scoping.
# `owner_field` already applies to GET/LIST/PATCH/DELETE.owner_field is the cheapest defense against IDOR (insecure direct object reference) bugs in CRUD APIs. Set it on every resource where rows belong to a specific user and the framework will scope GET/LIST/PATCH/DELETE automatically.
0-mcp by Stamatios Stamou Jr — github.com/ssjunior/0-mcp