Vela MCP Server
by natori-hrj
README.md
# Vela
[](https://github.com/natori-hrj/vela-mcp/actions/workflows/ci.yml)
[](./LICENSE)
**Governed, agent-agnostic data exploration over [MCP Apps](https://blog.modelcontextprotocol.io/posts/2026-01-26-mcp-apps/).**
Ask a question in natural language through your company's approved agent — Vela runs a *safe, permission-scoped* query and returns an **interactive chart right inside the chat**.
> One build, every agent. Because Vela is a standard MCP server, the same setup works in Claude Code, Claude Desktop, ChatGPT, Codex, VS Code — anything that speaks MCP.
```
business user ──▶ company's agent ──▶ Vela MCP server ──▶ your data source
"revenue by · semantic layer (Postgres / DuckDB
region last · row-level security / CSV / Parquet)
month" · PII masking
· read-only + audit
◀──────── interactive chart in chat ◀────────
```
## Demo

Charts are auto-selected from the shape of the query and rendered interactively (type switcher top-right). See them yourself with no host needed:
```bash
npm run build:ui && open dist/ui/preview.html
```
## Why
Text-to-SQL demos are easy. What stops them reaching production is everything *around* the query: who is allowed to see which rows, what "revenue" actually means, keeping PII out of the model, and proving what ran. Vela puts that governance in the server and hands business users a chat box.
- **Semantic layer** — the agent can only reference metrics/dimensions an admin defined. It never writes raw SQL, so it can't hallucinate joins or scan whole tables.
- **Row-level security** — filters are injected based on the caller's role, taken from the trusted session (never a tool argument the model could forge).
- **PII masking** — sensitive columns are hashed unless the caller's role is explicitly allowed to see them.
- **Read-only + audit** — every query runs read-only and is logged (who, what spec, row count, duration).
- **Charts in chat** — results render as an interactive chart via MCP Apps, not a wall of numbers.
## Quickstart
```bash
npm install
npm run smoke # exercises the engine against the bundled sample data
npm start # builds the UI and starts the MCP server (stdio)
```
`npm run smoke` prints a checklist proving chart selection, row-level security, PII masking, and the semantic boundary all work against `examples/data/orders.csv` — no database required.
### See the charts
```bash
npm run build:ui
open dist/ui/preview.html # standalone preview of the chart types, no host needed
```
### Connect it to an agent
Add Vela as an MCP server (Claude Desktop / Claude Code shown; any MCP host is similar):
```jsonc
{
"mcpServers": {
"vela": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/vela-mcp/src/server.ts"],
"env": {
"VELA_SEMANTIC": "/absolute/path/to/your/semantic.yaml",
"VELA_ROLE": "sales_west",
"VELA_AUDIT_LOG": "/var/log/vela-audit.jsonl"
}
}
}
}
```
Then ask: *"What was revenue by region last month?"* — the agent calls `list_metrics`, then `explore`, and a chart appears in the conversation.
> **Just trying it out?** Omit `VELA_SEMANTIC` and `VELA_ROLE` to run against the bundled sample data as `viewer` — no database or config needed. The server anchors to its own install directory, so it works no matter which directory the host launches it from.
### Two transports: local (stdio) and remote/browser (HTTP)
The config above uses **stdio** — local hosts (Claude Desktop, VS Code, Goose, Codex, Cursor) spawn Vela as a subprocess. Browser and hosted clients (claude.ai custom connectors, ChatGPT) can't launch a local process; they connect to a URL. Run Vela over **Streamable HTTP** for those:
```bash
npm run start:http # listens on :3000/mcp (set VELA_HTTP_PORT to change)
```
For a quick browser test, expose it with a tunnel and register the public URL (`https://<host>/mcp`) as a custom connector in your client's settings:
```bash
npx cloudflared tunnel --url http://localhost:3000 # or: ngrok http 3000
```
> ⚠️ The HTTP endpoint has no built-in auth. For anything beyond local testing, put it behind an authenticating proxy and derive the role from the authenticated identity — never trust a client-supplied role.
## The semantic layer (the one file an admin writes)
```yaml
sources:
- name: sales_db
adapter: postgres
dsnEnv: SALES_DB_DSN # secrets come from env, never this file
models:
- name: orders
source: sales_db
table: public.orders
dimensions:
- { name: region, column: region, type: string }
- { name: ordered, column: ordered_at, type: time }
- { name: customer, column: email, type: string }
measures:
- { name: revenue, sql: "sum(amount)", type: number }
- { name: order_count, sql: "count(*)", type: number }
access:
pii_mask:
- { column: email, unmask: [admin] } # hashed for everyone else
row_filters:
- { role: sales_west, where: "region = 'WEST'" }
```
Business users never see this. They just chat.
## Tools exposed to the agent
| Tool | Purpose |
|------|---------|
| `list_metrics` | What the current caller (by role) may explore — masked columns and active row filters are flagged. |
| `explore` | Submit a structured query spec (metrics, dimensions, filters, time grain) → get a chart. **No raw SQL crosses this boundary.** |
The `explore` query spec:
```jsonc
{
"model": "orders",
"measures": ["revenue"],
"dimensions": ["region"],
"filters": [{ "dimension": "ordered", "op": "last", "value": "30d" }],
"timeGrain": "day",
"limit": 1000
}
```
## Chart auto-selection
Vela picks a chart from the *shape* of the result, the way an analyst would (the user can switch types in the UI):
| Result shape | Chart |
|---|---|
| single measure, no dimension | KPI card |
| a time dimension | line (2nd categorical dim → series) |
| two measures + a label | scatter |
| one categorical dimension | bar |
| two categorical dimensions | grouped bar |
## Host support
Vela has two layers that light up independently:
- **Tool execution — works today, everywhere.** Any MCP host (Claude Desktop, claude.ai, Claude Code, Codex, VS Code, Cursor, …) can call `list_metrics` / `explore` and get a governed, permission-scoped answer over stdio or HTTP. This is the core value and it works now.
- **Interactive chart rendering — needs a GUI host with MCP Apps UI support.** The in-chat chart is a [MCP Apps](https://blog.modelcontextprotocol.io/posts/2026-01-26-mcp-apps/) widget (shipped Jan 2026); support is still rolling out. Terminal/CLI hosts can't render HTML widgets at all — the chart target is graphical hosts (desktop apps, web, IDE webviews). Vela's UI is spec-correct and standalone-verified (see the demo above); it renders as soon as a host executes MCP Apps widgets — no code change on Vela's side.
## Architecture
```
src/
semantic/ schema + loader + compiler (spec → parameterized SQL)
adapters/ duckdb, postgres, behind one interface
guards/ read-only / single-SELECT enforcement
chart/ shape → chart-type selection
audit/ append-only JSONL trail
ui/ render.ts (pure SVG renderer) + chart.ts (MCP App wiring)
engine.ts the governed core (no MCP dependency — unit-testable)
runtime.ts shared setup + MCP server factory (tools/resources)
server.ts stdio entrypoint (local hosts)
http.ts Streamable HTTP entrypoint (remote / browser hosts)
```
The **engine is independent of MCP**, so all the safety logic is exercised directly by `scripts/smoke.ts`. `scripts/mcp-check.ts` drives the real server as an MCP client.
## Security model
- The caller's **role comes from the session** (`VELA_ROLE` for local/stdio; an authenticated identity in a real deployment) — an agent cannot escalate its own permissions via tool arguments.
- Every compiled statement is asserted to be a **single read-only SELECT** before it runs; user-supplied values are **always parameterized**.
- Postgres queries additionally run inside a `READ ONLY` transaction.
- Secrets live in **environment variables**, never in the semantic file.
## Roadmap
Vela is open core (Apache-2.0). The safety-critical pieces live in the OSS core so self-hosting is genuinely production-safe. Planned:
- More adapters (BigQuery, Snowflake, MySQL) on the same interface
- Import metrics from existing semantic layers (dbt / Cube)
- SSO/SCIM identity, fine-grained policy, and centralized audit (enterprise)
## License
[Apache-2.0](./LICENSE)
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues