From a database to a working MCP server in one command

Point 0-mcp init at any MySQL or Postgres database and walk away with a complete Django + 0-mcp project: REST API, OpenAPI docs, MCP server, all wired up, all running locally in minutes. No model files to write, no resource boilerplate, no manual schema mapping.

The promise

You already have a database. It has tables, foreign keys, constraints, indexes โ€” real business schema, painstakingly built. Every traditional way to expose it to an LLM agent looks the same: write a model file (manually copy each column type), write a resource (manually choose what to expose), write a tool definition (manually map parameters), write the auth wiring, write the deployment glue. That's days of work for code an LLM should be able to consume in an hour.

0-mcp init collapses that to one command:

pip install 'django-zeromcp[gen-mysql]'   # or [gen-postgres]
0-mcp init

Run with no arguments and the CLI walks you through it interactively:

0-mcp โ€” interactive mode (Ctrl+C to abort)

Database engine (mysql/postgres) [mysql]:
Host [127.0.0.1]: db.internal
Port [3306]:
Database name: billing
User: app
Password: ********
Output directory [./billing]:
Let Django manage the schema (makemigrations + fake-initial on first run)? [y/N]:
Generate writable resources (POST/PATCH/DELETE)? [y/N]:

  โš   read-only mode blocks every non-GET request, including the
     MCP JSON-RPC endpoint (POST /mcp). Agents will get 405 on every
     tools/call. Use --writable (or rerun and answer "y" here) if you
     need a working MCP server.

introspected 80 tables, exposed 69, generated 99 files in billing
  โ†ณ 11 internal Django tables hidden (django_*, auth_*) โ€” not an error.

next steps:
  cd ./billing
  ./run.sh                              # or: python manage.py runserver

  # REST โ€” list rows from `client`:
  curl -s http://localhost:8000/client | jq

  # MCP โ€” call the matching tool:
  curl -s -X POST http://localhost:8000/mcp \
    -H 'Content-Type: application/json' \
    -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_client","arguments":{}}}' | jq

no X-Api-Key needed โ€” DEFAULT_AUTHENTICATED=False on the demo project.

โš   this project was generated read-only โ€” MCP `tools/call` requests will
   return 405 because the endpoint is POST. Re-run with `--writable` (or
   drop `MCP['READ_ONLY']` in settings.py) to expose a working MCP server.
see ./billing/README.md for full docs.

โœจ done in 9.4s.

In ~10 seconds you get a Django project that:

First run

Inside the generated project:

cd ./billing
./run.sh
๐Ÿ“ฆ

run.sh requires uv on PATH. Install once with curl -LsSf https://astral.sh/uv/install.sh | sh (or brew install uv). You also need a Redis server 6.2 or newer reachable at the host/port in .env โ€” the framework uses GETEX for sliding session TTLs.

The script:

  1. Creates a uv virtual environment if one doesn't exist.
  2. Installs requirements.txt.
  3. Runs makemigrations + migrate --fake-initial (only when you opted into Django-managed mode).
  4. Starts python manage.py runserver.

Open http://localhost:8000/. Done.

What you get at http://localhost:8000

Both surfaces share the same resource definitions, so what an agent sees and what a developer sees stay in sync โ€” forever.

Project layout

.
โ”œโ”€โ”€ .env                    # populated with the credentials you typed
โ”œโ”€โ”€ .env.example            # placeholders, safe to commit
โ”œโ”€โ”€ .gitignore              # .env already listed
โ”œโ”€โ”€ README.md
โ”œโ”€โ”€ run.sh
โ”œโ”€โ”€ manage.py / asgi.py
โ”œโ”€โ”€ requirements.txt
โ”œโ”€โ”€ settings/
โ”‚   โ”œโ”€โ”€ env.py              # auto-loads .env on import
โ”‚   โ””โ”€โ”€ settings.py         # MCP dict + INSTALLED_APPS + DATABASES
โ”œโ”€โ”€ router/
โ”‚   โ”œโ”€โ”€ endpoints.py        # `{ClassName}Resource โ†’ URL pattern` map
โ”‚   โ””โ”€โ”€ urls.py
โ””โ”€โ”€ modules/
    โ””โ”€โ”€ <prefix>/
        โ”œโ”€โ”€ apps.py         # AppConfig (label = prefix)
        โ”œโ”€โ”€ models.py       # one Django model per table
        โ””โ”€โ”€ resources.py    # one BaseResource per model

Tables sharing a name prefix land in the same module. contract, contract_item, contract_history all live in modules/contract/. Generated resources are intentionally minimal:

from zeromcp import BaseResource
from .models import User


class UserResource(BaseResource):
    model = User
    sensitive_fields = ['password', 'api_key'] # masked on every response

The two flags that matter

--writable

Default is read-only. The generated settings.py carries MCP = {'READ_ONLY': True, ...} โ€” a single global gate that rejects every non-GET request with 405 across all resources (REST and MCP alike). An agent pointed at a fresh project can never mutate the database by accident. Pass --writable (or answer "y" to the interactive prompt) to drop the gate and enable POST/PATCH/DELETE.

โš 

The MCP protocol is JSON-RPC over POST /mcp, so a read-only project's MCP server returns 405 on every tools/call. The REST GET endpoints still serve agents that only read, but if you need a working MCP surface, generate the project with --writable (or remove READ_ONLY from settings.py later).

--django-managed

Default keeps the database external: every model has Meta.managed = False, Django reads but never issues DDL, your existing migration tool stays in charge.

Pass --django-managed (or answer "y" to the prompt) to hand the schema to Django:

Filtering the table set

By default every business table is exposed and Django system tables are hidden. Use globs to shape the set:

# Only the billing-core tables
0-mcp init --db mysql://... -o ./billing --include 'client*,invoice*,contract*'

# Hide audit / queue / temporary tables
0-mcp init --db mysql://... -o ./billing --exclude 'audit_*,tmp_*,*_queue'

Auto-applied opinions

The generator emits the minimum viable scaffolding โ€” every default it picks should be one you'd never have to undo. Concretely:

Left for you (every guess here would be wrong sooner or later)

The two-phase flow

0-mcp init is a shortcut for three steps:

# 1. Read schema โ†’ JSON (Django-agnostic, diffable)
0-mcp introspect --db <url> -o introspection.json

# 2. Build a starter config you can edit
0-mcp config introspection.json -o config.yaml

# 3. Render config + templates โ†’ project tree
0-mcp generate config.yaml -o ./myproject

The middle step is where you customise. config.yaml is intentionally small and human-friendly:

project:
  name: billing
  backend: mysql
  database: billing

tables:
  client:
    expose: true
    db_table: client
    sensitive: [internal_notes]      # masked on REST and MCP

  contract:
    expose: true
    db_table: contract

  audit_log:
    expose: false                    # opt out of MCP/REST entirely

  django_migrations:
    expose: false

Everything else (column types, FKs, choices) is recomputed from introspection.json at generate time โ€” the config carries opinions, not state.

Re-running the generator

Schema drifted? Add a column, drop a table, add a constraint? Re-run:

0-mcp introspect --db <url> -o introspection.json
0-mcp generate config.yaml -o ./myproject

Existing files are overwritten. Don't hand-edit the generated files โ€” keep your customisations in resource subclasses outside modules/<prefix>/, or extend the framework via hooks. Treat the generated tree as build output.

Production checklist

Generated projects are demo-friendly so the first ./run.sh boots without ceremony. Before exposing the project to a network you don't fully control:

  1. .env โ€” flip DEBUG=false, set ALLOWED_HOSTS=app.example.com,โ€ฆ, rotate DJANGO_SECRET_KEY.
  2. router/urls.py โ€” replace the demo block with urlpatterns = get_routes(endpoints, mcp=True, docs_public=False) to gate /mcp, /docs and /openapi.json behind the same auth as the REST surface (X-Api-Key by default, Bearer if BEARER_RESOLVER is configured).
  3. settings/settings.py โ€” flip MCP['DEFAULT_AUTHENTICATED'] = True so every BaseResource starts requiring auth.
  4. TENANT_USER_API_MODEL โ€” point at the model that stores your API keys (column: api_key). Without it, MCP/REST traffic has no way to authenticate.

When to use this

You should run 0-mcp init when

You should not run it when

Why we built this

Standing up an MCP server for an existing database is the kind of work that should be measured in coffee breaks, not sprints. Every column type your DB already declared. Every relationship the FK constraints already encode. Every label the verbose_name already names. None of it should need a second human pass to expose to an agent.

0-mcp init is the inevitable shortcut.

See also

0-mcp by Stamatios Stamou Jr โ€” github.com/ssjunior/0-mcp