Skip to main content
Glama
rishi-ciq

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues