# sql-safe-mcp
A read-only, PII-safe SQL Server, MySQL and MariaDB MCP server for coding agents: schema knowledge and safe queries, with no way to change or leak data.
[](https://github.com/proprock/sql-safe-mcp/actions/workflows/ci.yml)
[](https://github.com/proprock/sql-safe-mcp/releases)
[](https://pypi.org/project/sql-safe-mcp/)
[](https://www.python.org/downloads/)
[](LICENSE)
[](https://modelcontextprotocol.io)
[](server.json)
Read the data. Protect the identity.
General-purpose database MCP servers hand the agent a raw SQL prompt and dozens of tools. This
server gives a coding agent the schema knowledge it needs to write correct code - servers,
databases, tables, columns, keys, indexes, stored procedures - and, on servers you mark
`pii_safe`, a way to look at real rows without ever seeing the personal data in them.
## Read-only by design. PII-safe by default
- **Read-only by construction** - seven tools, all annotated read-only. No tool writes data, and
the server never executes SQL an agent wrote: metadata comes from SQLAlchemy Inspector and fixed
catalog queries, and `execute_sql` runs only a validated, regenerated `SELECT`.
- **PII-safe by default** - on a `pii_safe` server, the columns you configure come back as
alias-bound, authenticated tokens (`pii:v1:...`), never as plaintext. An agent can still
project, count, and filter on them with `=` and `IN` using tokens it was given, so it can follow
a record without reading it. Tokens do not work on another server alias or with another key.
- **Fails closed** - SQL validation is an allowlist. Unknown syntax, unresolved lineage, and
unsupported protected-value types are refused, not guessed at. The verification evidence is in
the [security model](SECURITY-MODEL.md).
- **Least access first** - `access_level: metadata` (the default) exposes schema only;
`execute_sql` needs an explicit `pii_safe` alias with its own key. Database permissions stay the
primary control, so use a least-privilege login.
## Also
- **Your aliases, not your network** - the agent sees only the server aliases you configure. There
is no network discovery, and the catalog is not published as MCP resources.
- **Secrets stay out of sight** - connection URLs live in YAML with `${NAME}` placeholders resolved
from the environment. They never appear in logs or model-visible errors.
- **Compact, predictable output** - object-rooted results with stable sorting, literal
case-insensitive name filters, and stored procedure lists that do not expand definitions.
- **Errors an agent can act on** - an ambiguous name lists the candidate schemas. Errors never
contain connection details, credentials, keys, tokens, or rows.
- **Tested against attacks, not just examples** - the SQL and PII boundary is checked with an
adversarial corpus of hostile statements, property-based tests (token isolation, tamper
resistance), a live attack run against a really writable login with before/after snapshots, and
mutation testing. Results are in
[Verification of the SQL boundary](SECURITY-MODEL.md#verification-of-the-sql-boundary).
- **On PyPI** - `uvx sql-safe-mcp`, no repo clone required.
| Tool | Access | Purpose |
|---|---|---|
| `list_servers` | 🟢 read | Configured server aliases |
| `list_databases` | 🟢 read | Databases visible to the credentials |
| `list_tables` | 🟢 read | Base tables, filtered by schema or name |
| `get_table_definition` | 🟢 read | Columns, keys, constraints, and indexes of one table |
| `list_stored_procedures` | 🟢 read | Stored procedures, without definitions |
| `get_stored_procedure` | 🟢 read | The definition of one stored procedure |
| `execute_sql` | 🟢 read | One restricted `SELECT` on a `pii_safe` server; protected columns return tokens |
> [!NOTE]
> **Status:** SQL Server supports every tool. MySQL and MariaDB (`engine: mysql` or `mariadb`,
> `mysql+pymysql` URLs) support every tool too. `schema` is always `null` there because the
> database is the catalog, and `execute_sql` uses `LIMIT` instead of `TOP`. See
> [ARCHITECTURE.md](ARCHITECTURE.md).
- [Install](#install)
- [Configure](#configure)
- [PII-safe queries](#pii-safe-queries)
- [Security](#security)
- [Contributing](#contributing)
More detail lives in [`docs/`](docs): the [configuration reference](docs/configuration.md),
[what the tools return](docs/tools.md), and the [security model](SECURITY-MODEL.md).
## Install
```bash
uvx sql-safe-mcp
```
or
```bash
pip install sql-safe-mcp
```
Pin a version when you want a fixed surface: `uvx sql-safe-mcp==1.2.0`.
Requires Python 3.12+, [uv](https://docs.astral.sh/uv/) (or `pip`), and
[Microsoft ODBC Driver 18 for SQL Server](https://learn.microsoft.com/sql/connect/odbc/download-odbc-driver-for-sql-server)
when you connect to SQL Server. MySQL and MariaDB use the bundled PyMySQL driver and need nothing
else.
Verified against SQL Server 2022, MySQL 8.4, and MariaDB 11.4 (see [CHECKS.md](CHECKS.md)).
## Configure
Copy [sql-safe-mcp.example.yaml](sql-safe-mcp.example.yaml) to `sql-safe-mcp.yaml`, list your
servers, and keep credentials in environment variables:
```yaml
version: 1
servers:
reporting:
engine: sqlserver
access_level: metadata
connection_url: "${REPORTING_SQL_URL}"
```
`connection_url` is a SQLAlchemy URL whose dialect must match `engine`. It is a secret, so keep the
credentials in environment variables and reference them with `${NAME}`:
```yaml
servers:
# The whole URL comes from one variable (it may hold any valid URL).
reporting:
engine: sqlserver
connection_url: "${REPORTING_SQL_URL}"
# SQL Server (mssql+pyodbc), URL assembled from parts. Embedded placeholders are
# URL-encoded, so a password containing @ or / is safe.
billing:
engine: sqlserver
connection_url: >-
mssql+pyodbc://${BILLING_USER}:${BILLING_PASSWORD}@${BILLING_HOST}/master
?driver=ODBC+Driver+18+for+SQL+Server&Encrypt=yes
# MySQL or MariaDB (mysql+pymysql). Use engine: mariadb for MariaDB.
shop:
engine: mysql
connection_url: "mysql+pymysql://${SHOP_USER}:${SHOP_PASSWORD}@db.internal/shop"
```
A missing variable, or an `engine` that does not match the URL dialect, stops the server at startup.
Point the server at the file with `SQL_SAFE_MCP_CONFIG` (or `--config`), and check it without
connecting to any database:
```bash
SQL_SAFE_MCP_CONFIG=sql-safe-mcp.yaml uvx sql-safe-mcp --check-config
```
Configuration is validated at startup, and an error names the problem without printing a URL or
secret. Keep credentials in the host's own configuration and never commit them. The server acts
with the database account's permissions, so use a dedicated login with the least access the job
needs. Every setting, including the runtime limits, is in
[configuration.md](docs/configuration.md).