Skip to main content
Glama
RayaanKaz

mcp-sqlite-insights

by RayaanKaz
README.md
<div align="center">

# mcp-sqlite-insights

**Let Claude explore any SQLite database without ever being able to change it.**

[![tests](https://github.com/RayaanKaz/mcp-sqlite-insights/actions/workflows/tests.yml/badge.svg)](https://github.com/RayaanKaz/mcp-sqlite-insights/actions/workflows/tests.yml)
![Python 3.10 to 3.13](https://img.shields.io/badge/python-3.10%20%E2%80%93%203.13-3776AB?logo=python&logoColor=white)
![MCP Python SDK v2](https://img.shields.io/badge/MCP%20SDK-v2-5A45FF)
![Read-only](https://img.shields.io/badge/database%20access-read--only-2EA043)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)

[Quick start](#quick-start) · [Connect to Claude](#connect-to-claude) · [Tools](#tools) · [Security model](#security-model) · [Configuration](#configuration)

</div>

---

A [Model Context Protocol](https://modelcontextprotocol.io) server that gives Claude Desktop, Claude Code and any other MCP client two tools: one that **maps a SQLite database's structure** and one that **runs read-only SQL**. Point it at a `.db` file and ask questions in plain English:

> *Which product category brought in the most revenue this year?*
> *Find customers who signed up but never placed an order.*
> *Is `orders.customer_id` indexed? Show me the query plan for looking up one customer's orders.*

- **Read-only by construction.** Four independent layers, including SQLite's own read-only mode and a native authorizer, so a write is refused even if the model tries one.
- **Context-friendly.** Results are Markdown tables capped at 50 rows, long values are truncated, and slow queries are cancelled after 10 seconds.
- **Zero config.** With no database configured it creates a realistic demo database, so you can try it immediately.
- **One dependency.** Just the official [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk); SQLite support ships with Python.

### Why read-only matters

Agents like Claude Code increasingly run tools on their own. A database tool that *can* write is one mistaken `DELETE` away from data loss, and an instruction in a prompt is not a security boundary. This server makes writes impossible at the SQLite level, so you can let Claude explore a database without reviewing every query first.

---

## Quick start

Requires Python 3.10 or newer.

**With [uv](https://docs.astral.sh/uv/)** (no virtual environment needed; dependencies are declared inside `server.py`):

```bash
git clone https://github.com/RayaanKaz/mcp-sqlite-insights.git
cd mcp-sqlite-insights
uv run server.py --check
```

**With pip:**

```bash
git clone https://github.com/RayaanKaz/mcp-sqlite-insights.git
cd mcp-sqlite-insights
python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
python -m pip install -r requirements.txt
python server.py --check
```

`--check` creates the demo database, runs both tools, and proves that a `DELETE` is blocked. It ends with `Self-check passed.`

---

## Connect to Claude

### Claude Desktop

Open **Settings → Developer → Edit Config** and add the server to `claude_desktop_config.json`. Use **absolute paths**: Claude Desktop does not use your shell's `PATH` or activate virtual environments.

**macOS / Linux (pip install):**

```json
{
  "mcpServers": {
    "sqlite-insights": {
      "command": "/absolute/path/to/mcp-sqlite-insights/.venv/bin/python",
      "args": ["/absolute/path/to/mcp-sqlite-insights/server.py"]
    }
  }
}
```

<details>
<summary><b>Windows (pip install)</b></summary>

```json
{
  "mcpServers": {
    "sqlite-insights": {
      "command": "C:\\Users\\you\\mcp-sqlite-insights\\.venv\\Scripts\\python.exe",
      "args": ["C:\\Users\\you\\mcp-sqlite-insights\\server.py"]
    }
  }
}
```

Backslashes must be doubled inside JSON strings.
</details>

<details>
<summary><b>Using uv instead</b></summary>

Find uv's full path with `which uv` (macOS/Linux) or `where uv` (Windows), then:

```json
{
  "mcpServers": {
    "sqlite-insights": {
      "command": "/absolute/path/to/uv",
      "args": ["run", "/absolute/path/to/mcp-sqlite-insights/server.py"]
    }
  }
}
```
</details>

**Use your own database** by adding an `env` block (or `"--db", "/path/to/file.db"` at the end of `args`):

```json
      "env": { "SQLITE_INSIGHTS_DB": "/absolute/path/to/your.db" }
```

Quit Claude Desktop completely and reopen it. The two tools appear under the tools menu in the chat box.

### Claude Code

```bash
claude mcp add --transport stdio sqlite-insights -- \
  /absolute/path/to/mcp-sqlite-insights/.venv/bin/python \
  /absolute/path/to/mcp-sqlite-insights/server.py --db /absolute/path/to/your.db
```

Leave out `--db ...` to use the demo database.

---

## Tools

| Tool | Arguments | Returns |
|---|---|---|
| `inspect_schema` | `table_name` *(optional)* | Overview of every table and view (row and column counts), then each table's columns, types, primary keys, NOT NULL, defaults, foreign keys and indexes. With `table_name`: that table only, plus 3 sample rows showing real data formats. |
| `execute_query` | `sql` | Result as a Markdown table: at most 50 rows, with a note when more rows matched, plus timing. |

Both tools declare MCP annotations `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true` and `openWorldHint: false`. The server also sends instructions telling Claude to inspect the schema first and to prefer aggregates over raw rows.

Example `execute_query` result:

```
| month | orders | revenue |
|---|---|---|
| 2025-01 | 9 | 3114.43 |
| 2025-02 | 8 | 1538.38 |
| 2025-03 | 12 | 4353.85 |

_3 row(s) returned (0.6 ms)._
```

---

## Security model

```mermaid
flowchart LR
    C["Claude Desktop / Claude Code"] -- "MCP over stdio" --> S1
    subgraph SRV["server.py"]
        S1["1. Query sanitizer"] --> S2["2. Open file with mode=ro"]
        S2 --> S3["3. PRAGMA query_only = ON"]
        S3 --> S4["4. SQLite authorizer"]
    end
    S4 --> DB[("your.db")]
    DB -- "max 50 rows, 10 s limit" --> C
```

| Layer | Mechanism | Stops |
|---|---|---|
| 1. Query sanitizer | Exactly one statement, starting with `SELECT`, `WITH`, `VALUES` or `EXPLAIN`, with no write or admin keywords. String literals and comments are masked first, so `WHERE note = 'DELETE'` is not a false positive. | `DROP TABLE`, `SELECT 1; DELETE ...`, `WITH x AS (...) DELETE ...` |
| 2. Read-only open | The file is opened through a `file:...?mode=ro` URI. | Any write to the file, and creating a file that does not exist |
| 3. `query_only` | `PRAGMA query_only = ON` on every connection. | Writes to the main database and to anything attached |
| 4. Authorizer | A native SQLite callback that allows only read operations while each statement is compiled. | Anything unexpected that gets past layer 1, including `load_extension` |

The test suite attacks each native layer with the others disabled, and checks the database file's hash before and after.

**Additional hardening:** `trusted_schema = OFF`; SQLite defensive mode (Python 3.12+); `ATTACH` disabled outright (Python 3.11+); a fresh connection for every call; a 10-second query timeout; memory-bounded fetching (at most 51 rows are ever read from a result); 300-character cell truncation; logs on stderr only.

**What it does not do:**

- **Read-only is not private.** Every row a query returns becomes part of your conversation with the model. Don't connect a database containing data you are not allowed to share with your AI provider.
- It can read every table in the file. There is no per-table or per-column allowlist yet ([contributions welcome](#contributing)).
- It is built for local, single-user use over stdio, with no network transport or authentication.

---

## Configuration

| Setting | How | Default |
|---|---|---|
| Database file | `--db PATH` or the `SQLITE_INSIGHTS_DB` environment variable (`--db` wins) | `demo.db`, created next to `server.py` |
| Row cap | `MAX_ROWS` in `server.py` | 50 |
| Query timeout | `QUERY_TIMEOUT_SECONDS` | 10 seconds |
| Cell length | `MAX_CELL_CHARS` | 300 characters |

The **demo database** is a small shop: `customers` (40), `products` (15), `orders` (200), `order_items` (508) and a `monthly_revenue` view, with foreign keys, indexes and a few NULLs. It is generated from a fixed seed, so every machine gets identical data.

---

## Development

```bash
python -m unittest -v
```

29 tests cover sanitizer bypass attempts, each read-only layer on its own, the timeout, the row cap, Markdown escaping, awkward file names and identifiers, and a full stdio round trip through an MCP client. CI runs them on Linux, Windows, and macOS (Apple Silicon and Intel) with Python 3.10 to 3.13.

## Contributing

Issues and pull requests are welcome. Some ideas:

- Per-table and per-column allowlists (for example, hiding an `email` column)
- Publishing to PyPI so the server can be launched with `uvx`
- Support for more than one database at a time

Please include a test with any change to the read-only layers.

## License

[MIT](LICENSE). Built on the official [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk). This is a community project and is not affiliated with or endorsed by Anthropic.