databricks-readonly-mcp
by rishi-ciq
README.md
# databricks-readonly-mcp
A Model Context Protocol (MCP) server that gives an LLM **read-only** access to
Databricks. It exposes Unity Catalog browsing and SQL execution as tools, with
two layers of defence against accidental writes:
1. **In-process AST guard.** Every SQL statement is parsed with `sqlglot` and
rejected unless its root and entire tree are read-only
(`SELECT` / `WITH` / `SHOW` / `DESCRIBE` / `EXPLAIN` / set-operations).
`INSERT`, `UPDATE`, `DELETE`, `MERGE`, `DROP`, `CREATE`, `ALTER`,
`TRUNCATE`, `USE`, `SET`, `GRANT`, `COPY`, `REFRESH`, `OPTIMIZE`, `VACUUM`
— all rejected before the HTTP call is made. CTE-wrapped writes (e.g.
`WITH x AS (DELETE …) SELECT * FROM x`) are caught by a deep tree walk.
2. **Read-only Databricks principal (recommended).** Run the server under a
service principal or PAT that has only `USE CATALOG` / `USE SCHEMA` /
`SELECT` grants. See *Read-only principal* below.
Transport is stdio only.
## Install
```bash
pip install -e .
# or once published:
# uvx databricks-readonly-mcp
```
## Configure
Create `~/.databricks-mcp/config.yaml` (or point `DATABRICKS_MCP_CONFIG` at a
custom path). Two auth methods are supported per profile:
### `databricks_cli` (recommended)
Delegates to a profile in `~/.databrickscfg` via the Databricks SDK. Host
and credentials (PAT or OAuth U2M) come from there, and the SDK refreshes
OAuth tokens automatically. Set up once with the Databricks CLI:
```bash
databricks auth login --host https://your-workspace.cloud.databricks.com \
--profile ciq-beta
```
Then reference it in the MCP config:
```yaml
default_profile: beta
profiles:
beta:
warehouse_name: esm_etl_wh # or warehouse_id: bd6e1945574f6d03
default_catalog: client_catalog
max_rows: 1000
auth:
method: databricks_cli
cli_profile: ciq-beta # name in ~/.databrickscfg
```
No tokens in YAML, no env vars. Good for laptops / interactive use.
**Warehouse selection** is one of (highest precedence first):
| Field | Behavior |
|------------------|-----------------------------------------------------------------------------|
| `warehouse_id` | Pin a specific warehouse. No lookup; fastest startup. |
| `http_path` | `/sql/1.0/warehouses/<id>` — the `<id>` is used as `warehouse_id`. |
| `warehouse_name` | Resolved at startup via `/api/2.0/sql/warehouses`. Case-insensitive; must match exactly one. |
| *(omit all)* | Auto-pick: succeeds only if exactly one warehouse is visible to your principal. |
If you don't know which warehouse to pin, list them with:
```bash
databricks-readonly-mcp --profile beta --list-warehouses
```
Or pick one interactively (prompts on TTY, prints a YAML snippet to paste):
```bash
databricks-readonly-mcp --profile beta --pick-warehouse
```
Both modes run as one-shot CLI commands and exit; they don't start the MCP
server.
### `token` (for CI, headless servers)
Reads a PAT from the env var named in `token_env`. `host` must be set:
```yaml
profiles:
prod:
host: https://your-prod-workspace.cloud.databricks.com
warehouse_id: prodwarehouseid
auth:
method: token
token_env: DATABRICKS_TOKEN_PROD # name of the env var; NOT the token
default_catalog: client_catalog
max_rows: 1000
```
See [`examples/config.example.yaml`](examples/config.example.yaml) for a
complete template.
Profile selection precedence: `--profile` flag > `DATABRICKS_MCP_PROFILE` env
var > `default_profile` in the config.
## Run
```bash
databricks-readonly-mcp --profile beta
# or
python -m databricks_mcp --profile beta
```
## Wire into Claude Code / Claude Desktop
Add to your `mcpServers` config (e.g. `~/.claude.json`):
When using `auth.method: databricks_cli` (recommended), no `env` block is
needed — credentials come from `~/.databrickscfg`:
```json
{
"mcpServers": {
"databricks": {
"command": "databricks-readonly-mcp",
"args": ["--profile", "beta"]
}
}
}
```
With `auth.method: token`, supply the env var:
```json
{
"mcpServers": {
"databricks": {
"command": "databricks-readonly-mcp",
"args": ["--profile", "prod"],
"env": { "DATABRICKS_TOKEN_PROD": "dapi…" }
}
}
}
```
Restart Claude Code; the tools appear under the `databricks` server in `/mcp`.
## Tools
| Tool | Args | Returns |
|------------------|---------------------------------|-------------------------------------------------------------------------|
| `list_catalogs` | — | `{catalogs: [{name, comment, owner, catalog_type}]}` |
| `list_schemas` | `catalog` | `{schemas: [{name, full_name, comment, owner}]}` |
| `list_tables` | `catalog, schema` | `{tables: [{name, full_name, table_type, data_source_format, comment}]}`|
| `describe_table` | `full_name` | `{full_name, table_type, columns: […], properties}` |
| `run_sql` | `query, row_limit?` | `{columns, rows, row_count, truncated, statement_id, execution_time_ms}` <br/> Returns `{error: "ReadOnlyViolation", message}` when blocked. |
`row_limit` is always clamped to the profile's `max_rows`.
## Read-only principal (recommended)
The AST guard is robust but software. Put a second wall up at the data plane
by giving the server a principal that *cannot* write, even if asked to:
```sql
-- Run as a workspace admin once, per environment.
GRANT USE CATALOG ON CATALOG client_catalog TO `databricks-mcp-readonly`;
GRANT USE SCHEMA ON ALL SCHEMAS IN CATALOG client_catalog TO `databricks-mcp-readonly`;
GRANT SELECT ON ALL TABLES IN CATALOG client_catalog TO `databricks-mcp-readonly`;
GRANT CAN USE ON WAREHOUSE <warehouse_id> TO `databricks-mcp-readonly`;
-- explicitly: no MODIFY, no CREATE, no ALL PRIVILEGES, no warehouse mgmt.
```
> **Don't** point this MCP at a PAT for a user that has broader grants. The
> guard catches the obvious cases, but a parser bug in any combination
> AST-allow-listing + over-privileged principal is the weakest configuration.
## Development
```bash
python -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'
pytest
```
### Layout
```
src/databricks_mcp/
server.py stdio entrypoint, CLI parsing, FastMCP setup
tools.py tool functions registered onto FastMCP
client.py DatabricksReadClient — Statement API + Unity Catalog REST
sql_guard.py assert_readonly: sqlglot AST allow-list
config.py YAML loader, profile resolution
errors.py typed exceptions
tests/ pytest suite (sql_guard, config, client, tools)
```
## Out of scope (v1)
- Job / cluster / warehouse management tools — not exposed.
- HTTP / SSE transport — stdio only.
- Multi-statement bodies, transactions — refused.
- Anything that writes, anywhere — refused.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues