Metadata-Version: 2.5
Name: vibedasher-mcp
Version: 0.6.1
Summary: MCP server exposing the Vibedasher data engine (datasets, query, viz) to a customer's AI.
Project-URL: Homepage, https://vibedasher.com
Project-URL: Source, https://github.com/JulienGdnr/vibedasher
Author: Vibedasher
License: MIT
Keywords: analytics,bi,eject,mcp,model-context-protocol,vibedasher
Requires-Python: >=3.10
Requires-Dist: fastmcp>=2.0.0
Requires-Dist: httpx<0.29.0,>=0.23.0
Requires-Dist: keyring>=24.0.0
Requires-Dist: msgpack>=1.0.0
Requires-Dist: uvicorn>=0.30.0
Requires-Dist: vibedasher<5,>=4.0.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# vibedasher-mcp

An [MCP](https://modelcontextprotocol.io) 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](https://pypi.org/project/vibedasher/) 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, ...)` | Create a CSV dataset, upload, load, poll to READY. |
| `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](#delivery-an-ejected-tree-does-not-fit-in-one-response). |
| `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`. |

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

```python
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)

```bash
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.

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:

```json
{
  "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:

```bash
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)
```

```json
{
  "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

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

```bash
claude mcp add vibedasher --env VIBEDASHER_REGION=eu-central-1 -- vibedasher-mcp
```

Then ask your agent for something. It has 40 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."

For the JSON `mcpServers` form of both lanes, see [Auth](#auth).

## Development

The package depends on the sibling SDK at `../sdk/py`. Tests wire that path
automatically (`tests/conftest.py`), so from `packages/mcp`:

```bash
python -m pytest tests/ -q
```

> Note: `fastmcp` is a **client-side** dependency of this standalone package; it is
> not bundled into the API Lambdas, so the `export_requirements.py` step in the root
> `CLAUDE.md` does not apply here.
