Databricks MCP Server
by OliveriGuido
README.md
# Databricks MCP Server — Natural-Language Analytics POC
A small [Model Context Protocol](https://modelcontextprotocol.io) server that lets
an LLM client (e.g. **Claude Desktop**) answer **business questions in natural
language** over a Databricks dataset — without writing SQL by hand.
It runs against the public `samples.nyctaxi.trips` dataset that ships with every
Databricks workspace, so it's reproducible by anyone.
## What it exposes (the three MCP primitives)
| Primitive | Name | Purpose |
|-----------|------|---------|
| **Tool** | `run_query` | Executes a **read-only** SQL query against `samples.nyctaxi.trips` and returns the rows. |
| **Resource** | `schema://nyctaxi` | Curated schema + **metric definitions** and gotchas — the context layer that makes the generated SQL correct. |
| **Prompts** | `revenue_by_month`, `busiest_pickup_zones`, `trips_by_hour`, `fare_distance_summary` | Ready-made business questions. |
## Safety / governance
Two layers, on purpose:
1. **App-level guard** (`is_read_only`): only a single `SELECT`/`WITH` statement is
accepted; any write/DDL keyword (`INSERT`, `UPDATE`, `DROP`, ...) is rejected,
and a `LIMIT 1000` is appended when missing.
2. **The real guarantee**: connect with a Databricks token whose grants are
**read-only** on the catalog. App guards reduce footguns; permissions are what
actually protect the data. Never give an LLM a write-capable credential.
## Architecture
```
Claude Desktop ──stdio──► MCP server (this repo) ──Databricks SQL connector──► samples.nyctaxi.trips
(client) tool · resource · prompts (read-only)
```
`run_query` doesn't open the connection in-process — it shells out to
`query_runner.py` (`subprocess.run(..., stdin=subprocess.DEVNULL, capture_output=True)`).
See the note below for why.
## Implementation note: why `run_query` uses a subprocess
Both points were reproduced and verified on **Windows + the FastMCP `stdio`
transport** (Claude Desktop and the MCP Inspector). Symptom in both: the tool call
hangs and the client returns `MCP error -32001: Request timed out` at ~60s, even
though the same query runs in ~4s with the connector directly.
1. **`sql.connect()` stalls ~60s when called *inside* the server process.** From a
clean child process it connects in ~2s; inside the FastMCP process it blocks
until the client's request times out. It stalls on the event-loop thread *and*
on a worker thread, so it's a process-level interaction with the connector — not
just the event loop being blocked. Running the query in a child process avoids
it. (Disabling telemetry / `use_cloud_fetch` does **not** help.)
2. **`stdin=subprocess.DEVNULL` is required on the child.** A stdio MCP server's own
stdin *is* the JSON-RPC pipe from the client. A child started with the default
`stdin=None` inherits that pipe handle and hangs until the client gives up
(~60s). Detaching stdin makes it return at query speed. `capture_output=True`
already detaches stdout/stderr — stdin is the one that's easy to miss, so piping
the query out to a subprocess without it does **not** fix the hang.
> **Gotcha — don't launch the Inspector from Git Bash on Windows.** MSYS2 rewrites
> the POSIX-looking `DATABRICKS_HTTP_PATH` (`/sql/1.0/warehouses/…` →
> `C:/Program Files/Git/sql/1.0/warehouses/…`), so the server gets a **404**, not a
> timeout. Use PowerShell or `cmd`. Claude Desktop passes env vars directly and is
> unaffected.
## Run it
Prereqs: Python 3.11+, [`uv`](https://docs.astral.sh/uv/), a Databricks workspace
with a running SQL Warehouse and the `samples` catalog.
**Windows / PowerShell** (recommended on Windows — see the Git Bash gotcha above):
```powershell
cd "C:\path\to\databricks-mcp"
uv sync # first time only
# from SQL Warehouses -> Connection details, plus a personal access token.
# These live only in THIS PowerShell window (nothing is written to disk):
$env:DATABRICKS_HOST = "dbc-xxxxxxxx-xxxx.cloud.databricks.com"
$env:DATABRICKS_HTTP_PATH = "/sql/1.0/warehouses/xxxxxxxxxxxxxxxx"
$env:DATABRICKS_TOKEN = "dapixxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# launch the browser inspector, then run a query from its UI:
npx @modelcontextprotocol/inspector uv run server.py
```
<details>
<summary><b>macOS / Linux (bash)</b></summary>
```bash
uv sync
export DATABRICKS_HOST="adb-....azuredatabricks.net"
export DATABRICKS_HTTP_PATH="/sql/1.0/warehouses/...."
export DATABRICKS_TOKEN="dapi...."
npx @modelcontextprotocol/inspector uv run server.py
```
</details>
### Connect to Claude Desktop
You can reach the config file in two ways:
- **Via the UI (recommended):** in Claude Desktop go to
**Settings → Developer → Edit Config**. This opens (and creates, if missing)
`claude_desktop_config.json` in the right folder.
- **By path:** edit it directly at
`%APPDATA%\Claude\claude_desktop_config.json` (Windows) or
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS).
Copy the contents of `claude_desktop_config.example.json` into that file,
fill in your real values, and **restart Claude Desktop**. Then ask things like:
> "What were the busiest pickup zones, and how does monthly revenue trend?"
## Notes
- `samples.nyctaxi.trips` is a public Databricks dataset; no private data is used.
- Secrets live in env vars / the Claude Desktop config, both git-ignored.
TDQS
A4/5.0
Scored across 1 tool
Disambiguation5/5
With only one tool, there is no risk of confusion between tools. The tool's purpose is clearly defined as running a read-only SQL query on a specific table.
Naming Consistency5/5
With a single tool, naming consistency is not a concern. The tool name 'run_query' follows a common verb_noun pattern.
Tool Count1/5
The server is named 'Databricks MCP Server', implying access to a wide range of Databricks functionality, but only one tool for a single query on a fixed table is provided. This is an extreme mismatch in scope.
Completeness1/5
The tool set is severely incomplete for a Databricks server. It lacks operations for managing databases, tables, clusters, or running arbitrary SQL beyond the fixed sample table.
Maintenance
ActivityInactive
ResponsivenessNo issues