Metadata-Version: 2.1
Name: drf_react_by_schema_mcp
Version: 0.3.2
Summary: Generic, schema-driven MCP server for drf-react-by-schema projects.
Home-page: https://gitlab.com/eita/drf-react-by-schema/drf-react-by-schema-mcp
Author: Cooperativa EITA
Author-email: coletivo@eita.org.br
License: GNU General Public License version 3
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 3.2
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Requires-Python: >=3.10
Description-Content-Type: text/x-rst
License-File: LICENSE

.. image:: https://gitlab.com/eita/drf-react-by-schema/drf-react-by-schema-mcp/-/raw/master/banner_drf-react-by-schema-mcp_2171x345.png
  :width: 1280
  :alt: DRF And React By Schema - MCP

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 <https://opencode.ai>`_ 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
<https://pypi.org/project/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 <https://pypi.org/project/drf-react-by-schema/>`_) —
  generates the DRF API (serializers, viewsets, filters, schemas) from your
  models;
* **drf-react-by-schema-react** (`npm <https://www.npmjs.com/package/drf-react-by-schema>`_) —
  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 <https://modelcontextprotocol.io>`_ 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
-----------

1. Install:

   .. code-block:: bash

       pip install drf-react-by-schema-mcp

2. Add the app (after the OAuth package) and include the URLs:

   .. code-block:: python

       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
       ]

3. Mount the MCP endpoint (see `Mounting /mcp`_).

4. Configure (all keys optional):

   .. code-block:: python

       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``:

  .. code-block:: python

      # 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:

  .. code-block:: bash

      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):

.. code-block:: json

    "example": {
      "type": "remote",
      "url": "https://<host>/mcp",
      "enabled": true,
      "oauth": {}
    }

With `opencode <https://opencode.ai>`_ 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.

.. list-table::
   :header-rows: 1

   * - 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.

.. code-block:: python

    # 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 <https://pypi.org/project/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:

.. code-block:: python

    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.

.. list-table::
   :header-rows: 1

   * - 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.
