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:
- Maps every table to a Django model.
- Generates one
BaseResourceper table โ REST + MCP tools out of the box. - Groups tables by name prefix into
modules/<prefix>/socontract,contract_item,contract_historyland in the same folder. - Auto-flags sensitive columns (
password,token,secret,api_key,otp, โฆ) intosensitive_fieldsโ values are masked with'*********'on every response. - Hides Django system tables (
django_*,auth_*) by default. - Wires
INSTALLED_APPS,apps.py,endpoints.py,urls.py,settings.py,manage.py,asgi.pyand a starter.envpopulated with the credentials you typed. - Drops a
run.shthat handles the rest.
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:
- Creates a uv virtual environment if one doesn't exist.
- Installs
requirements.txt. - Runs
makemigrations+migrate --fake-initial(only when you opted into Django-managed mode). - Starts
python manage.py runserver.
Open http://localhost:8000/. Done.
What you get at http://localhost:8000
/mcp/toolsโ tool registry the agent will read, browseable./mcp/tools.jsonโ same registry as JSON./docsโ interactive REST docs (Scalar UI)./openapi.jsonโ OpenAPI 3.0.3 spec, ready for codegen./mcpโ JSON-RPC 2.0 endpoint (POST). Agents call tools through here.
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 modelTables 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 responseThe 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:
Meta.managed = Falseis dropped โ Django owns the tables.run.shrunsmakemigrations+migrate --fake-initialon first boot, recording the existing schema as already migrated.- From then on, evolve the project the standard Django way: edit a model,
makemigrations,migrate.
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:
- Foreign keys resolve across modules with
'app.Model'references โ Django's check framework passes on first run. - Field-name
_idsuffix stripped where Django expects it (db_idcolumn โdb = ForeignKey(...)field withdb_column='db_id'). - Python keyword collisions (
class,type,def, โฆ) get afield_prefix; the SQL column stays exact viadb_column. - MySQL
tinyint(1)becomesBooleanField; Postgres ENUM types becomeCharField(choices=...). - Composite primary keys emit
models.CompositePrimaryKey(Django 5.2+ โ projects pinDjango>=5.2). MasterAccountaccess,InvoiceInvoicehistory,ContractContractdiscountget cleaned up toMasterAccountAccess,InvoiceHistory,ContractDiscountโ no Django app-prefix duplication in class names.sensitive_fieldsfilled in for columns matchingpassword,token,secret,api_key,otp,pwd,passwd,_token,_secret,_api_key,_apikey,_password. Values come back as'*********'from REST and MCP โ never the actual content.expose: falsefor tables matchingdjango_*andauth_*patterns โ system tables don't show up in MCP/REST until you ask for them..envis auto-loaded bysettings/env.pyso projects run under fish/csh without a separatesource .envstep.
Left for you (every guess here would be wrong sooner or later)
filter_fields,search_fields,order_fieldsโ these depend on what your agent actually needs, not on what your DB has indexed.mcp_fk_expandโ only you know which FKs are small enough to expand inline (lookup tables) versus large enough to follow by reference.- Custom routes (
routes = [...]) โ anything beyond CRUD is a domain decision. cache_ttl,mcp_list_omit_null,mcp_edit_omit_nullโ performance/serialisation tuning the agent would have to undo if we guessed wrong.
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: falseEverything 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:
.envโ flipDEBUG=false, setALLOWED_HOSTS=app.example.com,โฆ, rotateDJANGO_SECRET_KEY.router/urls.pyโ replace the demo block withurlpatterns = get_routes(endpoints, mcp=True, docs_public=False)to gate/mcp,/docsand/openapi.jsonbehind the same auth as the REST surface (X-Api-Key by default, Bearer ifBEARER_RESOLVERis configured).settings/settings.pyโ flipMCP['DEFAULT_AUTHENTICATED'] = Trueso everyBaseResourcestarts requiring auth.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 have a real database with tables, FKs, indexes โ and you want LLM agents to talk to it.
- You're adding MCP support to a stack that doesn't have one and don't want to write 50 model files.
- You're prototyping an integration. Generate, demo, iterate.
- You need a quick REST + OpenAPI baseline alongside the MCP layer.
You should not run it when
- Your MySQL is 5.x โ Django 5.2 (the version generated projects pin) requires MySQL 8.0.11+. Upgrade the database first.
- You want every model + resource hand-tuned. The generator emits the minimum precisely so this case is easy: subclass and extend.
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
- Quickstart โ write a resource by hand, two minutes flat.
- BaseResource attributes โ every knob the framework exposes.
- Scripts and ad-hoc ORM access โ
from zeromcp import ormfor cron jobs and notebooks. - MCP server โ what the MCP runtime does and how tool calls dispatch.
0-mcp by Stamatios Stamou Jr โ github.com/ssjunior/0-mcp