Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

reviews:
auto_review:
enabled: true
drafts: true # Enable reviews on draft PRs
base_branches: ['.*'] # Enable reviews on all branches except the default
2 changes: 1 addition & 1 deletion docs/concepts/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ engine.create_model_from_query(

The saved model can then be queried by name like any other model — useful for materializing complex aggregations.

Via MCP, use the `create_model_from_query` tool. Via API, `POST /models/from_query`.
Via MCP, use `create_model` with a `query` parameter.

### Column naming in query-derived models

Expand Down
10 changes: 1 addition & 9 deletions docs/examples/02_sql_vs_dsl/sql_vs_dsl_nb.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -178,15 +178,7 @@
"cell_type": "markdown",
"id": "8150b965",
"metadata": {},
"source": [
"## Query Result as Model\n",
"\n",
"What if you want to use DSL transforms like `time_shift` or `change` to define derived measures or dimensions?\n",
"\n",
"Use `create_model_from_query()` to save a query's result as a permanent model. The derived columns become dimensions and measures on the new model, which can then be queried like any other - or just use a query inside another query, as covered in [multistage queries](../06_multistage_queries/multistage_queries.md)\n",
"\n",
"See [Creating Models from Queries](../../concepts/models.md#creating-models-from-queries) for details."
]
"source": "## Query Result as Model\n\nWhat if you want to use DSL transforms like `time_shift` or `change` to define derived measures or dimensions?\n\nUse `create_model()` with a `query` parameter to save a query's result as a permanent model. The derived columns become dimensions and measures on the new model, which can then be queried like any other - or just use a query inside another query, as covered in [multistage queries](../06_multistage_queries/multistage_queries.md)\n\nSee [Creating Models from Queries](../../concepts/models.md#creating-models-from-queries) for details."
},
{
"cell_type": "markdown",
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/07_aggregations/aggregations.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ The colon becomes an underscore in result keys:
| `revenue:avg` | `orders.revenue_avg` |
| `customers.*:count` | `orders.customers._count` |

When a query is saved as a model (`create_model_from_query`), these canonical names become the new model's column names.
When a query is saved as a model (`create_model` with a `query` parameter), these canonical names become the new model's column names.

---

Expand Down
95 changes: 60 additions & 35 deletions docs/getting-started/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,81 @@

Connect your AI agent (Claude Code, Cursor, etc.) to your database through SLayer's MCP server. No Python knowledge required.

## Install
## Prerequisites

Install [uv](https://docs.astral.sh/uv/getting-started/installation/) — the fast Python package manager. SLayer runs via `uvx` with no separate install step.

## Connect to your agent

### Claude Code

Register SLayer as an MCP server — Claude Code will spawn it automatically when needed:

```bash
uv tool install motley-slayer
claude mcp add slayer -- uvx --from 'motley-slayer[mcp]' slayer mcp --storage ./slayer_data
```

For databases other than SQLite, add the driver extra (see [full list](../configuration/datasources.md#database-drivers)):

```bash
uv tool install 'motley-slayer[postgres]'
claude mcp add slayer -- uvx --from 'motley-slayer[mcp,postgres]' slayer mcp --storage ./slayer_data
```

## Connect to your agent
### Other agents (JSON config)

### Claude Code (stdio — recommended)
Most MCP-compatible agents accept a JSON server configuration. Add this to your agent's MCP config file:

```bash
claude mcp add slayer -- slayer mcp --storage ./slayer_data
```json
{
"mcpServers": {
"slayer": {
"command": "uvx",
"args": ["--from", "motley-slayer[mcp,postgres]", "slayer", "mcp", "--storage", "./slayer_data"]
}
}
}
```

If SLayer is in a virtualenv, use the full path to the executable:
Replace `postgres` with your database driver, or use `motley-slayer[all]` for all supported databases.

### Remote / shared server

SLayer also supports HTTP/SSE transport for running on a different machine, in Docker, or sharing between multiple agents. See the [MCP Reference](../reference/mcp.md#sse-remote) for details.

### Verify

```bash
claude mcp add slayer -- $(which slayer) mcp --storage /absolute/path/to/slayer_data
claude mcp list
```

### Remote agents (HTTP/SSE)
## Connect a database

Start the server, then point your agent at the SSE endpoint:
The recommended approach is to drop a datasource YAML file into your storage folder. This keeps credentials out of the agent conversation and lets you use environment variable references.

```bash
slayer serve --storage ./slayer_data
Create a file at `slayer_data/datasources/mydb.yaml`:

# In another terminal / agent config:
claude mcp add slayer-remote --transport sse --url http://localhost:5143/mcp/sse
```yaml
name: mydb
type: postgres
host: ${DB_HOST}
port: 5432
database: ${DB_NAME}
username: ${DB_USER}
password: ${DB_PASSWORD}
schema_name: public
```

## Connect a database
`${...}` references are resolved from environment variables at read time. Set them in your shell before starting the agent, or use a `.env` file with your agent's environment configuration.

Once the agent is connected, it handles everything conversationally. A typical exchange:
Datasource configs are **hot-reloaded** — you can add or edit YAML files while the server is running, and the next MCP tool call will pick up the changes. No restart needed.

> **You:** Connect to my Postgres database at localhost, database "myapp", user "analyst"
>
> **Agent:** *calls `create_datasource` → auto-ingests models → calls `datasource_summary`*
>
> "Connected! I found 4 tables: orders (12 dims, 8 measures), customers (5 dims, 3 measures), ..."
>
> **You:** How many orders per status?
>
> **Agent:** *calls `query(source_model="orders", fields=["*:count"], dimensions=["status"])`*
Once the datasource file is in place, ask your agent:

The agent uses these MCP tools in order:
> "Ingest models from the mydb datasource and show me what's available"

1. `create_datasource` — connect to DB (auto-ingests models by default)
2. `datasource_summary` — discover available models and their schemas
3. `inspect_model` — see dimensions, measures, and sample data for a model
4. `query` — run queries
The agent will call `ingest_datasource_models` to generate models from the database schema, then `datasource_summary` to list them.

See the [MCP Reference](../reference/mcp.md) for the full tools list.
You can also create datasources conversationally via the `create_datasource` MCP tool — see the [MCP Reference](../reference/mcp.md#datasource-management) for details.

## Verify it works

Expand All @@ -70,6 +86,15 @@ Ask your agent:

The agent should call `datasource_summary` and return a list of your tables/models. If it says "no models found", check that:

1. The `--storage` path is correct
2. You've connected a datasource (or the agent has via `create_datasource`)
3. Models were ingested (auto-ingest runs by default with `create_datasource`)
1. The `--storage` path matches where your datasource YAML files are
2. Models have been ingested (via `ingest_datasource_models` or `create_datasource` with auto-ingest)
3. Environment variables referenced in the datasource config are set

## Alternative: permanent install

If you prefer a traditional install instead of `uvx`:

```bash
uv tool install 'motley-slayer[mcp,postgres]'
claude mcp add slayer -- slayer mcp --storage ./slayer_data
```
28 changes: 17 additions & 11 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,36 @@ SLayer is different: models are editable at runtime, aggregation is chosen at qu

## What it looks like

Given an `orders` [model](concepts/models.md) with a `revenue` measure and a join to `customers`:
Given an `orders` [model](concepts/models.md) with a `revenue` measure and joins to `customers` and `regions`:

```json
{
"source_model": "orders",
"fields": [
"revenue:sum",
{"formula": "revenue:sum / *:count", "name": "aov"},
{"formula": "change_pct(revenue:sum)", "name": "mom_growth"},
{"formula": "cumsum(revenue:sum)", "name": "running_total"},
"customers.score:avg"
{"formula": "revenue:sum / time_shift(revenue:sum, -1, 'year') - 1", "name": "yoy_growth"},
"customers.score:last(changed_at)"
],
"time_dimensions": [{"dimension": "created_at", "granularity": "month"}],
"filters": ["status = 'completed'", "change(revenue:sum) > 0"]
"dimensions": ["customers.regions.name"],
"time_dimensions": [{
"dimension": "created_at",
"granularity": "month",
"date_range": ["2025-01-01", "2025-12-31"]
}],
"filters": ["status = 'completed'", "change(revenue:sum) > 0"],
"order": [{"name": "revenue_sum", "direction": "desc"}]
}
```

One query, and SLayer handles:

- **`revenue:sum`** — aggregation is chosen at query time, not baked into the measure definition. The same `revenue` measure works with `sum`, `avg`, `median`, `weighted_avg`, or [any custom aggregation](examples/07_aggregations/aggregations.md). No measure proliferation.
- **`revenue:sum / *:count`** — arithmetic on aggregated measures, named inline.
- **`change_pct(revenue:sum)`** — month-over-month growth, computed as a window transform. SLayer has [built-in transforms](examples/04_time/time.md) for `cumsum`, `change`, `time_shift`, `rank`, `lag`, `lead` — all nestable (`"change(cumsum(revenue:sum))"` works).
- **`customers.score:avg`** — a measure from a [joined model](examples/05_joined_measures/joined_measures.md), resolved automatically by walking the join graph. No manual sub-query needed.
- **`change(revenue:sum) > 0`** — filtering on a computed transform, right in the filter string. SLayer figures out it needs to compute the transform first, then filter.
- **`revenue:sum`** — aggregation is chosen at query time, not baked into the measure definition. The same `revenue` measure works with `sum`, `avg`, `median`, `weighted_avg`, or [any custom aggregation](examples/07_aggregations/aggregations.md).
- **`change_pct(revenue:sum)`** — month-over-month growth as a [transform](examples/04_time/time.md). SLayer generates the necessary window query. Other built-in transforms: `cumsum`, `change`, `time_shift`, `rank`, `lag`, `lead` — all nestable (`"change(cumsum(revenue:sum))"` works).
- **`revenue:sum / time_shift(revenue:sum, -1, 'year') - 1`** — arithmetic on aggregated measures. `time_shift` runs a separate time-shifted sub-query and joins it back by all dimensions; dividing by it gives year-over-year growth. Standard operator precedence applies.
- **`customers.score:last(changed_at)`** — a measure from a [joined model](examples/05_joined_measures/joined_measures.md), resolved by walking the [join graph](examples/05_joins/joins.md). `last` is an aggregation that picks the latest record's value — `changed_at` tells it which column defines "latest."
- **`customers.regions.name`** — a multi-hop dimension: SLayer traces `orders → customers → regions` and builds the joins automatically.
- **`change(revenue:sum) > 0`** — filtering on a computed transform. SLayer computes the transform first as a hidden field, then applies the filter on the outer query.

## What SLayer does

Expand Down
3 changes: 1 addition & 2 deletions docs/interfaces/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ claude mcp list
|------|-------------|
| `datasource_summary` | List all datasources and their models with schemas (dimensions, measures). Returns JSON. |
| `inspect_model` | Detailed model info with sample data. Params: `model_name`, `num_rows` (default 3), `show_sql` (default false). |
| `create_model` | Create a new model from table/SQL definition. |
| `create_model_from_query` | Create a model from a query — saves the query's SQL as a reusable model with auto-introspected dimensions and measures. Params: `name`, `query` (SLayer query dict), `description` (optional). |
| `create_model` | Create a model from a table/SQL definition or from a query. Pass `sql_table`/`sql` with `dimensions`/`measures` for table-based, or pass `query` (a SLayer query dict) to auto-introspect dimensions and measures from the query result. |
| `edit_model` | Edit an existing model in one call. Params: `model_name` (required), `description`, `data_source`, `default_time_dimension` (optional metadata), `add_measures` (list), `add_dimensions` (list), `remove` (list of names). |
| `delete_model` | Delete a model entirely. |

Expand Down
70 changes: 37 additions & 33 deletions docs/reference/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,54 @@

SLayer runs as an [MCP](https://modelcontextprotocol.io/) server, allowing AI agents (Claude, Cursor, etc.) to discover and query data conversationally.

## Transports

SLayer supports two MCP transports. Both expose the exact same tools — the only difference is how the agent connects.

### Stdio (local)
## Quick Start

The agent spawns SLayer as a subprocess and communicates via stdin/stdout. You do **not** run `slayer mcp` manually — the agent launches it. You only need to register the command with your agent.
The fastest way to run SLayer is via `uvx` — no install needed. You only need [uv](https://docs.astral.sh/uv/getting-started/installation/).

**Claude Code setup:**
**Claude Code:**

```bash
claude mcp add slayer -- slayer mcp --models-dir ./slayer_data
claude mcp add slayer -- uvx --from 'motley-slayer[mcp,postgres]' slayer mcp --storage ./slayer_data
```

If `slayer` is installed in a virtualenv (e.g. via Poetry), use the full path to the executable so the agent can find it regardless of working directory:
**JSON config** (Claude Desktop, Cursor, and other MCP-compatible agents):

```json
{
"mcpServers": {
"slayer": {
"command": "uvx",
"args": ["--from", "motley-slayer[mcp,postgres]", "slayer", "mcp", "--storage", "./slayer_data"]
}
}
}
```

```bash
# Find the virtualenv path
poetry env info -p
# e.g. /home/user/.venvs/slayer-abc123
Replace `postgres` with your database driver (see [full list](../configuration/datasources.md#database-drivers)), or use `motley-slayer[all]` for all supported databases. SQLite works with just `motley-slayer[mcp]`.

# Register with the full path
claude mcp add slayer -- /home/user/.venvs/slayer-abc123/bin/slayer mcp --models-dir /path/to/slayer_data
```
See the [Getting Started guide](../getting-started/mcp.md) for full setup instructions including SSE/remote and permanent install options.

## Transports

SLayer supports two MCP transports. Both expose the exact same tools.

### Stdio (local — recommended)

The agent spawns SLayer as a subprocess and communicates via stdin/stdout. You do **not** run `slayer mcp` manually — the agent launches it. The `claude mcp add` and JSON config examples above both use this transport.

### SSE (remote)

MCP over HTTP via Server-Sent Events. You run `slayer serve` yourself — it exposes both the REST API and the MCP SSE endpoint on the same port:

```bash
# 1. Start the server
slayer serve --models-dir ./slayer_data
uvx --from 'motley-slayer[mcp,postgres]' slayer serve --storage ./slayer_data
# REST API at http://localhost:5143/
# MCP SSE at http://localhost:5143/mcp/sse
```

Then, in a separate terminal, register the remote endpoint with your agent:
Then register the remote endpoint with your agent:

```bash
# 2. Connect the agent
claude mcp add slayer-remote --transport sse --url http://localhost:5143/mcp/sse
```

Expand All @@ -65,16 +73,16 @@ claude mcp list
| `list_tables` | List tables in a database before ingesting. |
| `edit_datasource` | Edit an existing datasource config. |
| `delete_datasource` | Remove a datasource config. |
| `datasource_summary` | List all datasources and their models with schemas (dimensions, measures). Returns JSON. |
| `ingest_datasource_models` | Auto-generate models from DB schema with rollup joins. Params: `datasource_name`, `include_tables`, `schema_name`. |

### Model Management

| Tool | Description |
|------|-------------|
| `datasource_summary` | List all datasources and their models with schemas (dimensions, measures). Returns JSON. |
| `inspect_model` | Detailed model info with sample data. Params: `model_name`, `num_rows` (default 3), `show_sql` (default false). |
| `create_model` | Create a new model from table/SQL definition. |
| `create_model_from_query` | Create a model from a query — saves the query's SQL as a reusable model with auto-introspected dimensions and measures. Params: `name`, `query` (SLayer query dict), `description` (optional). |
| `edit_model` | Edit an existing model in one call. Params: `model_name` (required), `description`, `data_source`, `default_time_dimension` (optional metadata), `add_measures` (list), `add_dimensions` (list), `remove` (list of names). |
| `create_model` | Create a model from a table/SQL definition or from a query. Pass `sql_table`/`sql` with `dimensions`/`measures` for table-based, or pass `query` (a SLayer query dict) to auto-introspect dimensions and measures from the query result. |
| `edit_model` | Edit an existing model in one call. Supports upsert for dimensions, measures, aggregations, and joins (create if new, update if existing). Also manages scalar metadata and filters. See params below. |
| `delete_model` | Delete a model entirely. |

### Querying
Expand All @@ -101,12 +109,6 @@ claude mcp list
| `explain` | bool | Run EXPLAIN ANALYZE and return the query plan |
| `format` | string | Output format: `"markdown"` (default, compact), `"json"` (structured), or `"csv"` (most compact). Case-insensitive |

### Ingestion

| Tool | Description |
|------|-------------|
| `ingest_datasource_models` | Auto-generate models from DB schema with rollup joins. Params: `datasource_name`, `include_tables`, `schema_name`. |

## Typical Agent Workflows

### Connect and explore a new database
Expand Down Expand Up @@ -141,8 +143,10 @@ To explore first without auto-ingesting:
```
1. edit_model(
model_name="orders",
add_measures=[{"name": "avg_amount", "sql": "amount", "type": "avg"}],
add_dimensions=[{"name": "priority", "sql": "priority", "type": "string"}],
remove=["amount_sum"]
measures=[{"name": "avg_amount", "sql": "amount"}],
dimensions=[{"name": "priority", "sql": "priority", "type": "string"}],
remove={"measures": ["amount_sum"]}
)
```

Upsert semantics: if a measure/dimension/aggregation/join with that name already exists, only the provided fields are updated. To remove entities, use the `remove` dict keyed by type (`"dimensions"`, `"measures"`, `"aggregations"`, `"joins"`).
11 changes: 10 additions & 1 deletion slayer/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,18 @@ def get_connection_string(self) -> str:

def resolve_env_vars(self) -> "DatasourceConfig":
data = self.model_dump()
unresolved = []
for key, value in data.items():
if isinstance(value, str):
data[key] = _resolve_env_string(value)
resolved = _resolve_env_string(value)
data[key] = resolved
for match in re.finditer(r"\$\{(\w+)\}", resolved):
unresolved.append(match.group(1))
if unresolved:
raise ValueError(
f"Datasource '{self.name}': unresolved environment variable(s): "
f"{', '.join(unresolved)}"
)
return DatasourceConfig(**data)


Expand Down
Loading
Loading