Metadata-Version: 2.5
Name: vibedasher-mcp
Version: 0.5.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: msgpack>=1.0.0
Requires-Dist: uvicorn>=0.30.0
Requires-Dist: vibedasher>=2.4.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` — 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

API key only, via `X-Api-Key` (handled by the SDK's `create_client`). Set:

```bash
export VIBEDASHER_API_KEY=...           # mint via `POST /v1/api-keys` or the console
export VIBEDASHER_REGION=eu-central-1   # or us-east-1
# export VIBEDASHER_BASE_URL=...        # optional override (on-prem/staging)
```

The key is checked at startup: `vibedasher-mcp` refuses to start without it and
prints what to do on stderr, so a key-less server never connects and never
advertises tools it cannot serve. It is never logged or echoed in any 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        # (monorepo dev: also make `vibedasher` importable)
vibedasher-mcp                    # stdio MCP server
# or:  python -m vibedasher_mcp
```

MCP client config (Claude Code / Cursor):

```json
{
  "mcpServers": {
    "vibedasher": {
      "command": "vibedasher-mcp",
      "env": { "VIBEDASHER_API_KEY": "...", "VIBEDASHER_REGION": "eu-central-1" }
    }
  }
}
```

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