DRF And React By Schema - MCP
A generic, schema-driven MCP server for projects built with DRF And React By Schema (DRBS).
It exposes any DRBS project’s DRF API (/api/...) to MCP clients such as opencode through a small set of generic tools — consultar, resumo, criar, editar, minha_conta — plus the schema-aware knowledge/guia tools and optional per-project shortcuts, so an assistant can understand the project (models, fields, endpoints, conventions, reverse relations) and then read (and, when opted in, write) its data with no per-model tool explosion.
The server is a stateless proxy: it takes the caller’s OAuth 2.1 Bearer token (issued by drf-react-by-schema-oauth), forwards it to the project’s /api/... and lets the project’s own DRF permissions scope the data. It stores no credentials.
About the suite
drf-react-by-schema is a suite that builds headless Django + React applications directly from your models:
drf-react-by-schema-django (PyPI) — generates the DRF API (serializers, viewsets, filters, schemas) from your models;
drf-react-by-schema-react (npm) — consumes those schemas to build the React forms, grids and CRUD interfaces;
drf-react-by-schema-oauth — OAuth 2.1 authorization/resource server that issues the tokens both the API and this MCP server accept;
drf-react-by-schema-mcp (this package) — the generic MCP server that exposes the same API to MCP clients.
About this package
A DRBS project already exposes a rich DRF API generated from its models. This package mounts a FastMCP server that proxies that API to MCP clients, so an assistant can:
understand the system before acting (knowledge) — the project’s endpoints, conventions and per-model field schemas, fetched from the base package’s GET /drbs/schema/;
read any /api/... endpoint (consultar), count and sum filtered sets without downloading them (resumo) and identify the logged-in account (minha_conta);
write — opt-in per project — with criar (POST) and editar (PATCH); DELETE is never exposed.
It is deliberately generic: the tool surface is bounded by a fixed set of generic tools plus any project shortcuts, not one tool per model. The same OAuth token authenticates /mcp and /api/..., and the project’s DRF permissions and OAuth scopes (read/write) remain the authority.
Compatibility
The base DRBS package supports Django ≥ 3.2, and some deployments are still on Django 3.2. This package therefore supports Django 3.2 through 5.x (and the 2.x django-oauth-toolkit line pulled in by drf-react-by-schema-oauth). It requires Python ≥ 3.10 (the mcp SDK).
Pre-requisites
Django ≥ 3.2
djangorestframework
drf-react-by-schema-oauth (installed automatically) — provides the OAuth server and the DRF authentication/scopes
mcp (installed automatically)
Quick start
Install:
pip install drf-react-by-schema-mcpAdd the app (after the OAuth package) and include the URLs:
INSTALLED_APPS = [ ... 'oauth2_provider', 'drf_react_by_schema_oauth', 'drf_react_by_schema_mcp', ] urlpatterns = [ ... path('', include('drf_react_by_schema_oauth.urls')), path('', include('drf_react_by_schema_mcp.urls')), # Protected Resource Metadata ]Mount the MCP endpoint (see Mounting /mcp).
Configure (all keys optional):
DRBS_MCP = { 'WRITE': False, # read-only by default 'KNOWLEDGE': True, # schema-aware knowledge()/guia() + write validation 'RESOURCE_URL': '/mcp', 'SHORTCUTS': [], # dotted paths to project-specific tools 'EXTRA_QUERY_PARAMS': [], # querystring params this project reads itself }
Mounting /mcp
/mcp speaks the MCP streamable-http transport, a long-lived ASGI application (it cannot be served from a synchronous WSGI URLconf). There are two supported deployments:
Combined — one ASGI process serves both Django and /mcp:
# asgi.py from drf_react_by_schema_mcp.server import get_mcp_asgi_application application = get_mcp_asgi_application()Standalone — a second ASGI process serves only /mcp (point DRBS_MCP['API_URL'] at the Django host) and the reverse proxy routes /mcp to it:
python -m drf_react_by_schema_mcp.server # or: uvicorn drf_react_by_schema_mcp.server:get_mcp_asgi_application --factory
In both cases the endpoint is protected: an unauthenticated request answers 401 with WWW-Authenticate: Bearer resource_metadata="…", so an MCP client can discover the OAuth server (RFC 9728) and start the flow automatically.
Connecting an MCP client
Point a remote MCP client at https://<host>/mcp with OAuth (no secret file):
"example": {
"type": "remote",
"url": "https://<host>/mcp",
"enabled": true,
"oauth": {}
}
With opencode that is opencode mcp auth example, followed by opencode mcp debug example.
Tools
The generic tools mirror the DRF API. A list endpoint returns {"results": [...]} — one JSON object, never a bare list (a bare list would be split into one MCP content block per record) — while a detail/object endpoint returns the object and metadados=True returns the full {count, next, previous, sum_rows, facets, results} envelope. An idempotent upstream request (GET) is retried once when the connection drops mid-flight.
Tool |
What it does |
|---|---|
knowledge(modelo?, campos?, detalhe?, sum_rows?, app?, search?, limit?) |
The project schema: a compact overview (endpoints, conventions, stamp, project about) by default — narrow it with app/search/limit — or one model’s field schema, its reverse relations (related) and its query capabilities (query) with modelo. detalhe="resumo" (default) keeps only the semantics; campos projects the schema fields and/or selects response sections (schema, query, related, endpoint, sum_rows) — e.g. campos=["query"]; sum_rows=True adds the valid measures/dimensions (large, so opt-in). Registered only when KNOWLEDGE is on. |
guia(tema?) |
The workflow, ready-made recipes and common mistakes, as data — read it before building queries (tool descriptions are truncated by some harnesses, so the workflow cannot live only there). tema picks a section; an unknown theme returns the whole guide with a warning instead of failing. Registered only when KNOWLEDGE is on. |
minha_conta() |
GET /api/my_account/ — the logged-in account. |
consultar(caminho, parametros?, metadados?, page_size?, filtros?, incluir?, campos?) |
GET /api/... (list or object). parametros is the querystring; filtros=[{campo, operador, valor}] is the ergonomic grid filter; incluir=["<relacao>"] nests each record’s related rows; campos=["a", "b"] projects each record onto those fields (narrow a large payload without touching the API). |
resumo(caminho, campos?, parametros?, filtros?) |
{count, sum_rows, facets} for the whole filtered set, without the list. |
criar(caminho, dados) |
POST /api/... (registered only when WRITE is on). |
editar(caminho, registro_id, dados) |
PATCH /api/.../<id>/ (registered only when WRITE is on). |
Accepted querystring parametros (combinable): page/page_size, search (text), ordering (- for descending), sum_rows (comma-separated numeric fields), and the grid filter field / operator / value. The three filter keys may each be a list; items match positionally and combine with AND. Common operators: equals, contains, startsWith, endsWith, isAnyOf, is, not, after, before, onOrAfter, onOrBefore, entre ("start,end"), isEmpty, isNotEmpty. field accepts relation paths with __. Equality on a foreign key compares the related object’s label (__str__/nome/slug, the query.label_field), not its id; to filter by id use field="<fk>__id" with operator="is".
consultar/resumo also accept the structured filtros, which builds the three lists for you and is the form to prefer:
consultar("/api/retirada/", filtros=[{"campo": "cooperada__id",
"operador": "is", "valor": 1}])
Because DRF silently ignores a querystring param it does not understand, the read tools reject an unknown one instead of returning unfiltered rows: the error names the endpoint’s valid filter_fields and the correct grid-filter form (a foreign key passed as a plain param, e.g. {"cooperada": 1}, is the classic trap). A project whose viewsets read their own params declares them in DRBS_MCP['EXTRA_QUERY_PARAMS']. Pass field or filtros, not both.
An ordering term is validated the same way, against the endpoint’s query.ordering_fields: DRF drops an unknown term silently (ordering=-id on a model that does not order by id quietly keeps the default order), so the MCP rejects it and lists the fields the endpoint accepts.
consultar can also nest a record’s related rows with incluir: it reads the model’s reverse relations from the schema (related) and attaches the child rows under the relation name, so one call replaces a manual “find the child endpoint, then filter it by <fk>__id”:
# a retirada and its hour records, in one call
consultar("/api/retirada/6900/", incluir=["projetos"])
It costs one request per record per relation, so it is meant for a record (or a small page): above DRBS_MCP['MAX_INCLUIR'] records (default 25) the call is refused rather than fanning out, and a relation with more rows than one page raises instead of silently truncating. knowledge(modelo="Retirada") lists the available relation names under related.
sum_rows items may also carry facets: medida|dimensao[|dimensao...] [|limite] (|-separated). medida is a numeric field, count (COUNT(*), i.e. rows) or count_distinct:<campo> (COUNT(DISTINCT campo), distinct non-null values); each dimensao must be a foreign key, a field with choices or a boolean — and may be a nested forward relation with __ (e.g. categoria__regiao); a trailing all-digits part is the top-N limite (ordered by the measure). Each measure still reports its <medida>_total flat (with : replaced by _), and the grouped buckets come back under facets. Use count_distinct:<campo> to count entities (e.g. distinct parents) when a model has several rows per entity.
# one page of 2026 entries
consultar("/api/widget/",
{"field": "publicado_em", "operator": "entre",
"value": "2026-01-01,2026-12-31", "page": 1})
# count + sums for the whole filter, without the list
resumo("/api/widget/", ["preco", "peso"],
{"field": "publicado_em", "operator": "entre",
"value": "2026-01-01,2026-12-31"})
# nº de itens por categoria (e total de preço)
resumo("/api/widget/", ["preco", "count|categoria"])
# nº de categorias distintas por tipo (entidades, não linhas)
resumo("/api/widget/", ["count_distinct:categoria|tipo"])
# top-5 categorias por preço, tabela cruzada com tipo
resumo("/api/widget/", ["preco|categoria|5", "count|categoria|tipo"])
# nº de itens por região da categoria (relação aninhada)
resumo("/api/widget/", ["count|categoria__regiao"])
Schema awareness
Instead of guessing /api/<model>/ paths and field names, the assistant can ask the project what it looks like. knowledge reads the base package’s machine-readable schema endpoint, GET /drbs/schema/ (see drf-react-by-schema, the base package’s “Machine-readable schema” section), with the caller’s Bearer token:
knowledge() — a compact overview: every top-level endpoint (modelo/app_label/caminho/verbose names — narrow it with app=/search=/limit=, since a large project’s list is the biggest payload), the API conventions (pagination, ordering/search/sum_rows, grid-filter operators), the schema stamp and the project’s about (the project root’s ABOUT.md, when present).
knowledge(modelo="widget") — that model’s field schema (types, choices, required, read_only…, with related_model on each relation field), its query capabilities (query.filter_fields — what the grid filter accepts as campo/field —, query.label_field and query.search_fields/query.ordering_fields) and its reverse relations (related: the child endpoint, the linking field and the nested_path when one exists), so the assistant can find the rows that hang off a record instead of guessing an endpoint name. The valid sum_rows measures and dimensions (medidas/dimensoes/medidas_distintas) come only with sum_rows=True (the block is large). detalhe="resumo" (the default) keeps only each field’s semantics; detalhe="completo" adds the per-field documentation (description/help_text) and campos=[...] projects the schema onto the named fields or selects whole response sections (schema/query/related/endpoint/sum_rows). The name is matched in any case/style, as the endpoint name, the underlying Django model name, or an /api/... path.
guia() — the workflow as data: the conventions, the canonical loop (knowledge → knowledge(modelo) → consultar/resumo), ready-made recipes (last record; filter a relation by id or by label; range; aggregate by relation; expand a record’s related rows; discover sum_rows measures) and the common mistakes. guia(tema=...) returns one section; an unknown theme returns the whole guide with a warning rather than failing. It exists because a tool description is truncated by some harnesses (only the first paragraph may reach the model), so anything an assistant must know has to be reachable by a call.
The bundle is schema-only: the React-only TypeScript artifacts (interface/enums), the React-only field keys (ui_required, model_required, style…) and the auto-generated nested *Related* viewsets are deliberately not shipped — the reverse relations and the query capabilities above carry what an assistant needs instead.
The bundle is fetched once per MCP process and cached in memory, keyed by its stamp: later calls send If-None-Match and a 304 Not Modified reuses the cache, while a changed schema (new stamp) transparently replaces it. Nothing is written to disk and the schema is never exposed as an MCP resource — it reaches the agent only as knowledge() output.
When the project exposes /drbs/schema/, criar/editar validate the payload against the same schema before sending it — required fields, read_only fields, value types and choice options — and return a field-level error without calling the API if it is invalid (e.g. "preco: campo obrigatório ausente."). A valid payload is forwarded unchanged.
If the project does not expose the endpoint (drf-react-by-schema < 0.27 or SCHEMA_ENDPOINT off), knowledge reports it as unavailable and writes are not validated — the server keeps working as a plain proxy.
Shortcuts
Register project-specific tools with dotted paths; each is a callable taking the MCP Context first (exactly like the generic tools) and is exposed under its own name:
DRBS_MCP = {
"SHORTCUTS": ["myproject.mcp.saldo_ferias"],
# optionally limit/rename the surface:
"TOOLS": ["consultar", "resumo", {"ferias": "saldo_ferias"}],
}
Settings
DRBS_MCP is a single dict; every key is optional.
Key |
Default |
Meaning |
|---|---|---|
WRITE |
False |
Register the write tools (criar/editar). |
KNOWLEDGE |
True |
Register the schema-aware knowledge/guia tools and validate write payloads against /drbs/schema/ when the project exposes it. |
SCHEMA_PATH |
"/drbs/schema/" |
Path (or absolute URL) of the base package’s schema bundle endpoint. |
EXTRA_QUERY_PARAMS |
[] |
Querystring params this project’s endpoints legitimately read themselves (e.g. in a filter_by_callback). The read tools reject unknown params, so a project that reads its own must declare them here. |
MAX_INCLUIR |
25 |
Max records consultar(..., incluir=[...]) expands in one call (one request per record per relation). Above it the call is refused. |
TOOLS |
None |
Allowlist of tools to expose. None = all generic (minus writes when WRITE is off) plus every shortcut. A list limits and orders the surface; an entry may rename, e.g. {"buscar": "consultar"}. |
SHORTCUTS |
[] |
Dotted paths to project-specific tool callables. |
RESOURCE_URL |
"/mcp" |
Path (or absolute URL) of the protected resource. |
API_URL |
None |
Base URL the proxy forwards to. None = this request’s origin (the right value when combined with Django); set it for a standalone process. |
SERVER_NAME |
"drf-react-by-schema" |
Name advertised to MCP clients. |
READ_SCOPE / WRITE_SCOPE |
"read" / "write" |
Scope names advertised in the Protected Resource Metadata. |
VALIDATE_TOKEN |
True |
Validate the Bearer token against VALIDATE_PATH before handling a request (so an expired token yields 401). |
VALIDATE_PATH |
"/api/my_account/" |
API path used for that check. |
ALLOWED_HOSTS |
[] |
Transport Host allowlist (anti-DNS-rebinding); empty disables it. |
STATELESS_HTTP |
True |
Serve streamable-http without session state (a fresh transport per request), so a client reconnect after a restart never hits 404 Session not found. Set False only if a client needs server-initiated messages. |
HOST / PORT |
"127.0.0.1" / 8000 |
Bind address for the standalone entrypoint. |
TIMEOUT |
60 |
Upstream request timeout, in seconds. |
Security model
Stateless proxy — no credentials are stored server-side; the caller’s Bearer token is forwarded to /api/... on every call.
Read-only by default — writes require WRITE=True and a token carrying the write scope and the project’s DRF permissions.
No ``DELETE`` — only GET/POST/PATCH are exposed.
Schema is structure, not data — /drbs/schema/ carries the generated picture (models, fields, endpoints, static config) only: no row data and no secrets; it is fetched with the caller’s own token and the project’s existing auth.
Scope enforcement is the project’s: read for safe methods, write for unsafe ones, via drf_react_by_schema_oauth.permissions.HasScope.
License
GPLv3 — Cooperativa EITA.
Metadata
Release files for drf-react-by-schema-mcp 0.3.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| drf_react_by_schema_mcp-0.3.2.tar.gz | 636.3 kB | Without provenance | Details for drf_react_by_schema_mcp-0.3.2.tar.gz |
Release files / drf_react_by_schema_mcp-0.3.2.tar.gz
| Download URL | drf_react_by_schema_mcp-0.3.2.tar.gz |
|---|---|
| Size | 636.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
05769137e9111c839ca09ac805aad7c2e32253671c7150635438c887b96db425
|
|
BLAKE2b-256 checksum How to use checksums |
e50be6fbd828b4afe37d1f903e3ac39909767324b7ec63dadb2f656ed4b9a2d6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/5.0.0 CPython/3.12.3
|
Security for drf-react-by-schema-mcp 0.3.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.