model2data-studio CLI
Pull a saved model2data studio project and write its data, so a build can regenerate its fixtures instead of carrying a CSV that was right once.
The schema is designed in the studio, where a diagram and a preview make it worth designing. This is the part that runs afterwards, unattended, from the same saved project — so the seeds a test suite runs against are the ones the model says they should be, rather than whatever was committed the day someone last remembered to export.
It is deliberately thin: no DBML parser, no generator, no schema knowledge. The studio's API does all of that; this only knows how to ask, and where to put the answer.
Install
pip install model2data-studio # or run it without installing:
uvx model2data-studio --help
It needs an API key from a Pro account on the studio: see pricing. The client is MIT licensed; the studio it talks to is not open source.
Configuration
Two environment variables, both overridable by a flag:
| flag | default | |
|---|---|---|
MODEL2DATA_STUDIO_KEY |
--key |
required |
MODEL2DATA_STUDIO_URL |
--url |
https://studio.jbanalytica.com |
Create a key in the studio's account menu. The CLI sends it as a bearer token and nothing else — it never prints it, and it is the only thing that identifies the account, so put it in your CI provider's secrets rather than in a file.
Leave the key's access on Generate and export, which is the default and
covers everything below: reading a project, generating, exporting. A key with
Read only access is refused by generate with a 403 saying so. The two
levels above it are for things the CLI does not do — Generate and save adds
writing the project back, for a pipeline that pushes a schema up rather than
pulling data down, and Full access adds deleting a project and managing its
share links. A key that leaks out of a CI log can only do what it was given.
Commands
model2data-studio projects # id, name, rows, last edited
model2data-studio whoami # the plan behind the key, and what it has used
model2data-studio generate --project <id> --out seeds/
model2data-studio generate --project <id> --out . --format dbt --adapter duckdb
generate takes every setting from the saved project — row counts, per-table
overrides, seed, locale, name — and each flag overrides one of them:
--format csv|dbt |
seed CSVs, or a whole dbt project. dbt export is a Pro feature. |
--rows N |
the default row count for every table |
--rows-for TABLE=N |
one table's count, on top of the project's. Repeatable. |
--seed N |
pin the data. Same seed, same rows, forever. |
--as-of YYYY-MM-DD |
pin the day dates and timestamps are anchored on. Defaults to the project's, then today. |
--locale CODE |
which country's people and addresses to generate |
--business-hours / --no-business-hours |
weight timestamps toward weekdays and working hours. Pro. |
--growth N |
relative change in activity across the window, e.g. 0.5 for the end being half again as busy as the start. Pro. |
--seasonality N |
strength of an annual cycle peaking in Q4, 0.0-1.0. Pro. |
--skew N |
how unevenly child rows spread over their parents, 0.0-1.0. Pro. |
--name NAME |
the dbt project's name |
--adapter duckdb|postgres |
the warehouse the dbt project targets |
--force |
replace files already in --out |
--check |
compare against --out instead of writing to it |
Every run also prints a row count for each table it generated, read back out of the CSVs — a quick way to see what actually landed without opening a seed.
Shaping flags name one field at a time. --growth on its own overrides
just the growth trend; business hours and seasonality still come from the
project, the same way --rows-for orders=9 overrides one table without
resetting every other one to the base count. Shaping is a Pro feature — a free
plan's key gets a 402 naming the plan boundary if the project or a flag asks
for anything but the uniform defaults.
Pin a seed. A project without one generates different rows every run, which
means a diff on every build and no way back to yesterday's data. If the project
has no seed and you pass no --seed, the CLI picks one and prints it, so a run
that surprised you can at least be reproduced.
Pin the day too. Dates and timestamps are generated relative to an anchor
day, so a seed alone reproduces its rows only for as long as that day is the
same. The studio pins one whenever a project's seed is on — Dates as of, in
the generation settings — and the CLI reads it back, which is what makes a run
here reproduce the rows the studio previewed rather than merely yesterday's CLI
run. --as-of overrides it. A project with no day pinned anchors on today — at
midnight, so every run on the same day agrees — and the CLI prints the day it
used, the same way it prints a picked seed. A pipeline that --checks
committed fixtures, or that needs a failing build to be reproducible on a
laptop next week, wants the day pinned one way or the other; with --seed, the
two together name one dataset for good.
The project must have been opened in the studio at least once since the studio started saving its parse. The API never parses DBML — the browser does, and posts the result — so a project with no stored parse has nothing to generate from, and the CLI says so rather than failing later.
--check never writes into --out. It regenerates into a scratch
directory and diffs it against what is already there, printing what changed,
what is missing and what is left over, then exits 1 if they differ — so a CI
job can fail a build whose committed fixtures nobody regenerated after a schema
change, instead of shipping them stale. Exits 0 and prints Up to date: N files when they match.
Regenerating dbt seeds in CI
name: Seeds
on: [push]
jobs:
seeds:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- name: Regenerate the seeds from the model
env:
MODEL2DATA_STUDIO_KEY: ${{ secrets.MODEL2DATA_STUDIO_KEY }}
run: |
uvx model2data-studio generate \
--project ${{ vars.MODEL2DATA_PROJECT_ID }} \
--out seeds/ --force --seed 1 --as-of 2026-01-01
- name: Build against them
run: uv run dbt build --target ci
--force because the checkout already holds the last run's files, and --seed
with --as-of because a pipeline that regenerates its fixtures should still be
able to explain a failure — with a pinned seed and a pinned day, a failing build
is reproducible on a laptop, however long after it ran.
Nothing is committed back: the seeds exist for the length of the job, and the
model in the studio is the thing under version control. If you would rather
commit them, add a step that opens a pull request when git status comes back
dirty — that turns a schema change into a reviewable diff of the data it
produces.
Failing CI when the committed fixtures are stale
If seeds are committed rather than regenerated on every run, --check is the
step that catches someone editing the schema in the studio and forgetting to
pull the new fixtures into the repo:
- name: Check the committed seeds match the model
env:
MODEL2DATA_STUDIO_KEY: ${{ secrets.MODEL2DATA_STUDIO_KEY }}
run: |
uvx model2data-studio generate \
--project ${{ vars.MODEL2DATA_PROJECT_ID }} \
--out seeds/ --seed 1 --as-of 2026-01-01 --check
It never writes into seeds/ — only into a scratch directory it diffs against
what's there — so this is safe to run against the checkout as-is, no --force
needed. --as-of is what lets the check pass on any day: the committed seeds
were generated relative to one day, and without it a schema with a date column
would compare against rows anchored on today and fail at the first midnight.
The MCP server: generating from an agent instead of a shell
The same account, the same key, the same API — for the editor rather than the terminal. The studio is where a schema is designed, with a diagram and a preview to design it against; the agent in your editor is where the data is wanted, and asking it for "two hundred believable orders from my webshop model" should not mean pasting DBML into a chat.
uvx --from 'model2data-studio[mcp]' model2data-studio mcp # the SDK is an extra, see below
uvx fetches it on first use, so there is nothing to install beforehand. With
the package installed (pip install 'model2data-studio[mcp]'),
model2data-studio-mcp starts the same server. For Claude Code:
claude mcp add model2data-studio \
--env MODEL2DATA_STUDIO_KEY=m2d_... \
-- uvx --from 'model2data-studio[mcp]' model2data-studio mcp
and for anything that reads a JSON config (Claude Desktop, Cursor, Zed):
{
"mcpServers": {
"model2data-studio": {
"command": "uvx",
"args": ["--from", "model2data-studio[mcp]", "model2data-studio", "mcp"],
"env": { "MODEL2DATA_STUDIO_KEY": "m2d_..." }
}
}
}
MODEL2DATA_STUDIO_URL works here too, for a self-hosted studio.
Five tools:
account |
the plan and its caps, worth reading before asking for a lot of rows |
list_projects |
the ids to pass to everything else |
describe_project |
one project's tables, columns, refs and enums as text, generation hints included |
preview_data |
generate and return the rows themselves, capped at fifty a table |
write_data |
generate and write the files, as generate --out does |
preview_data is the one worth having. An agent that can read generated rows
can act on them — write an assertion against them, notice that shipped_at
precedes ordered_at, spot a column coming out as placeholder sentences. It
answers with the rows inline and says how many there really are; write_data is
for when it has decided and wants the fixtures on disk.
Both go through the same export.resolve the CLI's generate does, so an agent
and a pipeline asking for the same project with the same arguments get the same
rows — including the part where an unpinned project has a seed picked for it and
is told which one, so the answer can be reproduced afterwards.
The MCP SDK is an optional extra rather than a dependency, on purpose: the reason this package is two dependencies is that it installs on CI runners next to a build that has nothing to do with the studio, and a runner regenerating fixtures does not need an MCP SDK.
Developing
cd cli
uv sync --extra dev
uv run pytest
uv run ruff check .
The tests drive the real Typer app, and the MCP tools through the server's own
call_tool, answering their HTTP with an httpx.MockTransport — so there is
nothing to run and nothing to reach.
Releasing
Bump version in pyproject.toml, __version__ in
model2data_studio/__init__.py and both versions in server.json (the release
workflow refuses a tag that disagrees with any of them), merge, then tag the
merge commit on main and push the tag:
git tag cli-v0.1.0 && git push origin cli-v0.1.0
.github/workflows/release-cli.yml runs the tests, builds, publishes to PyPI
through trusted publishing (no token stored anywhere) and then lists the new
version in the MCP registry.
Release files for model2data-studio 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| model2data_studio-0.1.0.tar.gz | 102.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| model2data_studio-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 129.6 kB
Release files / model2data_studio-0.1.0.tar.gz
| Download URL | model2data_studio-0.1.0.tar.gz |
|---|---|
| Size | 102.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5155d6b9eb5520e4c92af11b16a17276ead17f81b37eae84c802fdc182afbcab
|
|
BLAKE2b-256 checksum How to use checksums |
c6644748ac7b05876b87ec7aabe06f61a39719fcaccbb503f48fd8de21b0b1d1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / model2data_studio-0.1.0-py3-none-any.whl
| Download URL | model2data_studio-0.1.0-py3-none-any.whl |
|---|---|
| Size | 27.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
db2637674a67015fd2f473d435da8d8ab76830e22dd44e1f235e541f758e8c5d
|
|
BLAKE2b-256 checksum How to use checksums |
8c7efec461d84e9c5ed15e3d2cd49da9b7362075c18b792249b3d67342154916
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|