vibedasher-mcp
An MCP server that exposes the Vibedasher data engine — datasets, CSV upload + ETL, headless SQL query, and dashboard (viz) management — as tools a customer's AI (Claude Code, Cursor, ...) can drive directly.
This is the control plane (VD-601, PIVOT/PLAN.md EP-6). It wraps the published
vibedasher Python SDK and calls only the public, metered /api/v1/*
API.
Install: pip install 'vibedasher-mcp>=0.6.0', then vibedasher login — no API
key to mint. Live on PyPI since 2026-08-09, as are
vibedasher (PyPI) and @vibedasher/client (npm).
The eject tools (VD-602) ship in this package, not separately: eject_viz,
eject_viz_files, eject_instructions and get_viz_files are registered by
register_eject_tools() in eject.py. They pull a viz's code plus an SDK-wiring
manifest so your AI can recreate the dashboard natively in your stack.
Tools
| Tool | What it does |
|---|---|
list_datasets |
List datasets the key can read (id, name, cleanSQLName, status). |
get_dataset(dataset_id) |
One dataset's metadata + column schema. |
upload_dataset(name, csv_content, ...) |
Inline. Create a CSV dataset from CSV text you pass as an argument, upload, load, poll to READY. Every byte is a tool argument — ~260k tokens per MB — so this is the lane for a small table you are writing by hand. |
upload_dataset_from_file(name, csv_path, ...) |
From disk. Same flow, but the bytes are read from an absolute local path and never enter the caller's context; you get back datasetId, cleanSQLName, sourcePath, sourceBytes. Use this for any CSV that is already a file. stdio only; absolute paths only; refuses a directory, a device, a missing or unreadable path, and anything over 512 MiB (VIBEDASHER_MCP_MAX_UPLOAD_BYTES). |
run_query(sql, dataset_ids, params, type) |
Explore. Inline alias-only SQL across your datasets (plural, joins allowed) → typed columns + rows in the response. type="wasm" returns a plan of presigned Parquet URLs instead of rows — the credential-free data lane for a standalone build, and the only call that builds a dataset's wasm extract. |
run_query_to_file(sql, output_path, dataset_ids, params, format, overwrite) |
Extract. Same query, but the rows are written to a local CSV/JSONL file and never enter the response — you get back path, rowCount, columns, bytes, truncated. stdio only; absolute paths only; never clobbers without overwrite=True. |
list_vizzes(include_unpublished=False) |
List dashboards. |
get_viz(viz_id) |
One viz's metadata. |
create_viz(name, dataset_ids, seed_id/dashboard_config, ...) |
Create a viz (AI dashboard-build entry). |
build_viz(viz_id, branch) |
Compile a viz branch into a renderable bundle. |
eject_viz(viz_id, branch, out_dir, overwrite, max_chars) |
(eject) The viz's runnable source tree + package.json + Tailwind config + wiring manifest. Pass out_dir — see Delivery. |
eject_viz_files(viz_id, paths, branch, out_dir, ...) |
(eject) Ejected file contents on demand — the follow-up call when eject_viz came back paged. |
eject_instructions() |
(eject) How to recreate an ejected dashboard in your app — the query contract, auth modes, region sharding, and the host-bundler traps. |
get_viz_files(viz_id, branch, out_dir, ...) |
(eject) Raw file tree for a viz branch. Same delivery budget as eject_viz. |
list_connections() |
(connections) Every data connection in the account — the warehouses, databases and SaaS sources this account can already read. Credentials come back masked by the server; a viewer sees an empty list rather than a 403. |
get_connection(connection_id) |
(connections) One connection: type, and the address it points at. Reads our stored row — it does not prove the connection works. |
test_connection(connection_id) |
(connections) Opens the connection for real and closes it. Takes no credential: the secret is merged server-side behind its own mask. |
get_connection_schema(connection_id, database) |
(connections) The databases and tables inside a connection. Dials the source live; not paginated. |
list_connection_tables(connection_id, database) |
(connections) Tables only — the fallback when get_connection_schema is refused because the credential cannot list databases. |
get_connection_table_schema(connection_id, table, database) |
(connections) One table's columns as the source's information_schema spells them. |
validate_connection_query(connection_id, sql, limit) |
(connections) Run a SELECT against the source itself and see real rows. Refuses anything that is not SELECT/WITH before it reaches the wire. |
create_dataset_from_connection(name, connection_id, sql, ...) |
(connections) Store the result of a SELECT as a dataset that re-reads its source. Takes no credential — the connection id references secrets the server already holds — and it is the only ingestion here whose dataset create_refresh will schedule. |
list_dbt_projects() |
(dbt) Every dbt project — and the only source of the project_id that create_refresh takes. Note the endpoint CREATES a "Default Project" when the account has none; the response says when that may have just happened. |
get_dbt_project(project_id) |
(dbt) One project: model/dataset counts, last run status, its own cronSchedule. Creates nothing. |
list_dbt_models(project_id) |
(dbt) Every model, with datasetId (what it produces) and sourceDatasetIds (what it reads) — the join between dbt and the rest of the surface. |
get_dbt_model(model_id) |
(dbt) One model with its sql and compiledSql. |
list_dbt_runs(project_id) |
(dbt) Run history: status, timing, model counters. logs is excluded here on purpose. |
get_dbt_run(project_id, run_id) |
(dbt) One run in full, with the tail of dbt's own output and a route to the per-model cause. |
Delivery: an ejected tree does not fit in one response
Measured against prod on 2026-08-18: eject_viz(1275) is 44 files / 517,351
characters (~129k tokens) and eject_viz(1282) is 45 files / 556,363
characters (~139k tokens). MCP clients cap a tool result, so the single-shot
payload was either dropped outright by a strict client or ate the agent's entire
context. Every eject response now carries a delivery block, and the three file
tools take the same three parameters.
| Mode | How | Cost |
|---|---|---|
| disk (preferred) | out_dir=<ABSOLUTE path> |
~30k characters regardless of tree size — measured 32,308 for viz 1275 |
| paged (default) | omit out_dir; files fills to max_chars (60,000), the rest land in delivery.pendingPaths |
10 calls for viz 1275, none over budget |
| unbounded | max_chars=0 |
the pre-0.2.0 payload, on request only |
eject_viz(viz_id=1275, out_dir="/abs/path/to/app/dashboard") # one call, whole tree on disk
out_dir resolves on the machine running the MCP server; with the standard
stdio launch (command: vibedasher-mcp) that is your machine, because the client
spawns the server as a local subprocess. Relative paths are refused, existing
files are refused unless overwrite=True, and any path escaping out_dir aborts
the write before a byte lands.
Paging is the fallback, not the goal: round trips do not reduce total tokens (the
content still crosses the model), they only keep each individual response
deliverable. Hand delivery.pendingPaths straight back to
eject_viz_files(viz_id, paths=...) and repeat until it is empty. A response
with a non-empty pendingPaths is a page, not a tree.
Override the budget per call with max_chars or globally with
VIBEDASHER_MCP_MAX_RESPONSE_CHARS.
Three data lanes
Pick by what you will do with the rows:
| Lane | Call | What comes back | Use it for |
|---|---|---|---|
| Explore | run_query(sql, ...) |
the rows, inline | aggregates, samples, schema checks — anything the agent will read |
| Extract | run_query_to_file(sql, output_path=<absolute>, format="csv"|"jsonl") |
{path, rowCount, columns, bytes, truncated} — never the rows |
anything loaded, charted or shipped from disk; thousands of rows and up |
| Static asset | run_query(sql, type="wasm") |
a plan of presigned Parquet URLs, no rows | a dashboard you host yourself, querying in-browser with DuckDB-WASM and no key in the bundle |
run_query_to_file writes on the machine running the MCP server, which with the
standard stdio launch is the agent's own machine (the client spawns the server as a
local subprocess). It is refused over --http, where the path would resolve on the
server's disk. Both lanes that return counts honour the backend's 100,000-row cap:
truncated: true means the rows — in the response or in the file — are a prefix.
Parquet output is deliberately not offered: the wasm lane already returns Parquet
the server built, and pyarrow is a 40 MB wheel to install for one option.
run_query reuses the SDK's hand-written query() transport (VD-301): the caller
never sees inline-vs-presigned delivery, MessagePack, or retries — one call in,
typed rows out. Each dataset id resolves server-side to that dataset's
cleanSQLName alias under the VD-203 RLS/alias-rewrite; SQL references only aliases.
Auth
Two lanes. The first one is the default because it asks nothing of you.
Sign in through your browser (first run)
pip install 'vibedasher-mcp>=0.6.0' # `login` does not exist below 0.6.0
vibedasher login # opens a browser; approve; done
vibedasher login opens your browser, captures the authorization code on a
loopback listener bound to 127.0.0.1, and stores a short-lived refresh token in
your OS keyring (with a 0600 file fallback where no keyring is available).
There is no key to mint, copy, paste or rotate, and nothing lands in a config
file or your shell history. Later runs are silent until the session lapses, at
which point login asks again. The refresh token is single-use and rotates on
every refresh; if a rotated-away copy is ever presented again — someone read the
keyring or the credentials file — the whole session is revoked on the spot and
the next run asks you to login again.
Two companions: vibedasher whoami prints the account, user, region and the date
you will be asked to sign in again; vibedasher logout revokes this machine's
session and forgets it.
The MCP client block then carries no credential at all:
{
"mcpServers": {
"vibedasher": {
"command": "vibedasher-mcp",
"env": { "VIBEDASHER_REGION": "eu-central-1" }
}
}
}
VIBEDASHER_REGION is optional here — login records the region the account
actually lives in — but pinning it costs nothing and makes the shard explicit.
API key (CI, servers, headless agents)
Anywhere a browser cannot open, an API key is still the answer. Mint one in the
console (Settings → API keys) or via POST /v1/api-keys, and give it to the
server as an environment variable:
export VIBEDASHER_API_KEY=... # X-Api-Key, handled by the SDK's create_client
export VIBEDASHER_REGION=eu-central-1 # or us-east-1
# export VIBEDASHER_BASE_URL=... # optional override (on-prem/staging)
{
"mcpServers": {
"vibedasher": {
"command": "vibedasher-mcp",
"env": {
"VIBEDASHER_API_KEY": "...",
"VIBEDASHER_REGION": "eu-central-1"
}
}
}
}
An API key authenticates as the user who created it, has no expiry unless one is set, and is a bearer secret in plaintext on whatever disk holds that config. That is a fine trade for a build server and a poor one for your laptop — which is why it is the second lane and not the first.
Either way, the server checks before it serves
vibedasher-mcp resolves a credential — stored session first, then
VIBEDASHER_API_KEY — at startup. With neither, it refuses to start and prints
what to do on stderr, so a credential-less server never connects and never
advertises tools it cannot serve. Neither credential is ever logged or echoed in
a tool result.
Metering
Every tool hits the public, metered endpoints (create_dataset is credit-gated;
run_query is metered via the usage/credits event). The MCP adds no side channel
and bypasses no metering.
Run
pip install 'vibedasher-mcp>=0.6.0' # (monorepo dev: also make `vibedasher` importable)
vibedasher login # sign in once, in a browser
vibedasher-mcp # stdio MCP server
# or: python -m vibedasher_mcp
Wiring Claude Code in one line, on a machine that has signed in:
claude mcp add vibedasher --env VIBEDASHER_REGION=eu-central-1 -- vibedasher-mcp
Then ask your agent for something. It has 82 tools; a good first prompt is
"run whoami, then list_datasets, and build me a dashboard from the most
interesting one — verify with get_build_status and probe_viz before telling
me it's done."
Two verifiers, two worlds, and they do not overlap:
probe_viz |
render_probe |
|
|---|---|---|
| reads | the GENERATED tree (config.ts + pages/*.tsx) |
nothing — it opens the published page |
| runs | each tile's own SQL, server-side | a real browser |
| answers | does the emitted SQL match the declared binding | did it paint, with what numbers, with what errors, and did it leak into the host |
| on a hand-authored tree | reports hand_authored_tree and checks nothing |
this is the one |
| never claims | that a viewer saw anything | that a number is correct |
render_probe walks the WHOLE dashboard, not one screenful: it scrolls the
frame to the bottom in steps (re-settling after each one, so a lazily-mounted
tile is counted and a permanently skeletal one still goes red) and, when the
tree exposes the generated page control [data-selector="page"] [data-value],
opens every page and sweeps each. Tiles are unioned by node identity, so a tile
carrying three conventions is one tile and two conventions marking different
tiles add up. tiles.covers states in one sentence what the count is a count
OF — all pages fully scrolled, one page fully scrolled, or one viewport — and
whatever it could not walk stays in notVerified. Pass scroll=False /
traverse_pages=False to narrow it; the caveat comes back when you do.
render_probe needs a browser, so Playwright is an extra:
pip install 'vibedasher-mcp[render]>=0.6.0' && python -m playwright install chromium
Without it the tool returns checked: false and that install command. It never
reports a pass it did not earn.
For the JSON mcpServers form of both lanes, see Auth.
Development
The package depends on the sibling SDK at ../sdk/py. Tests wire that path
automatically (tests/conftest.py), so from packages/mcp:
python -m pytest tests/ -q
Note:
fastmcpis a client-side dependency of this standalone package; it is not bundled into the API Lambdas, so theexport_requirements.pystep in the rootCLAUDE.mddoes not apply here.
Release files for vibedasher-mcp 0.9.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| vibedasher_mcp-0.9.1.tar.gz | 899.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vibedasher_mcp-0.9.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.4 MB
Release files / vibedasher_mcp-0.9.1.tar.gz
| Download URL | vibedasher_mcp-0.9.1.tar.gz |
|---|---|
| Size | 899.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b6206cdf542e335bfd68750a6174e1eb18862408d2d37bfeb34c9d550bb161fc
|
|
BLAKE2b-256 checksum How to use checksums |
e43d961661125c652eeb6fc2e39b28fff6482b6e6b0e9c78c2084cc3b14298d8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / vibedasher_mcp-0.9.1-py3-none-any.whl
| Download URL | vibedasher_mcp-0.9.1-py3-none-any.whl |
|---|---|
| Size | 481.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
90d9313cd2256e447df70d5a45dc769698fa3c2ec425e4aecd3307cdc9901278
|
|
BLAKE2b-256 checksum How to use checksums |
91bb220af290ec5dc5006a2f50e6bc763c3d085bc3c9325c6abdbb164700f5cb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|