Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The MCP MariaDB Server provides a Model Context Protocol (MCP) interface for man
- [Available Tools](#available-tools)
- [Embeddings & Vector Store](#embeddings--vector-store)
- [Configuration & Environment Variables](#configuration--environment-variables)
- [PII Filter (opt-in)](#pii-filter-opt-in)
- [Installation & Setup](#installation--setup)
- [Usage Examples](#usage-examples)
- [Integration - Claude desktop/Cursor/Windsurf](#integration---claude-desktopcursorwindsurf)
Expand Down Expand Up @@ -152,6 +153,8 @@ All configuration is via environment variables (typically set in a `.env` file):
| `HF_MODEL` | Open models from Huggingface | Yes (if EMBEDDING_PROVIDER=huggingface) | |
| `ALLOWED_ORIGINS` | Comma-separated list of allowed origins | No | Long list of allowed origins corresponding to local use of the server |
| `ALLOWED_HOSTS` | Comma-separated list of allowed hosts | No | `localhost,127.0.0.1` |
| `PII_FILTER_ENABLED` | Strip PII columns from tool responses (`true`/`false`) | No | `false` |
| `PII_CONFIG_PATH` | Absolute path to PII blocklist YAML | Yes (if `PII_FILTER_ENABLED=true`) | |

Note that if using 'http' or 'sse' as the transport, configuring authentication is important for security if you allow connections outside of localhost. Because different organizations use different authentication methods, the server does not provide a default authentication method. You will need to configure your own authentication method. Thankfully FastMCP provides a simple way to do this starting with version 2.12.1. See the [FastMCP documentation](https://gofastmcp.com/servers/auth/authentication#environment-configuration) for more information. We have provided an example configuration below.

Expand Down Expand Up @@ -229,6 +232,70 @@ export FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET="GOCSPX-..."

---

## PII Filter (opt-in)

The server can optionally strip Personally Identifiable Information (PII)
columns from tool responses before they leave the MCP server. The goal is to
**prevent an AI client from receiving PII values** — end users with direct
database access are explicitly out of scope (they are trusted to handle PII
appropriately).

### Behavior

When `PII_FILTER_ENABLED=true` and `PII_CONFIG_PATH` points at a YAML
blocklist:

- **`get_table_schema` / `get_table_schema_with_relations`**: blocked columns
are stripped from the schema response so the AI never learns their names.
- **`list_tables`**: tables marked fully-PII (`"*"`) are hidden.
- **`execute_sql`**:
- **Pre-flight check** refuses queries that would project a blocked column,
including `SELECT *` over a table with any blocked column. The error
message names the blocked column so the AI can rewrite the query.
- **Post-filter** strips blocked columns from result rows. This runs
regardless of the pre-flight outcome and is the actual security
guarantee — even an unparseable query that slips past the pre-flight
check will have blocked column names dropped from the returned rows.

Blocked columns are **still allowed in `WHERE`, `JOIN ON`, `GROUP BY`,
`ORDER BY`, and `HAVING`**. These do not produce PII values in the response,
so a query like `SELECT id FROM users WHERE email = ?` is permitted — the
result rows contain no PII.

### When disabled

When `PII_FILTER_ENABLED` is unset or `false`, every filter function
short-circuits in its first line. No YAML load, no SQL parsing, no per-row
work. The performance overhead in this state is a single boolean check per
tool call.

### Blocklist YAML format

See [`examples/pii_blocklist.example.yaml`](examples/pii_blocklist.example.yaml)
for a complete example. The schema is:

```yaml
database_name:
table_name:
- column_name
- other_column
another_table: "*" # entire table is PII; hidden from list_tables
```

Names are matched case-insensitively.

### Recommended setup

- Keep the blocklist YAML **outside this repository** (it enumerates your
sensitive columns). A private repo or restricted-share cloud-drive folder
both work.
- Use a per-environment env file: enable the filter in production, leave it
off in staging or local development.
- The YAML is loaded once per process and cached. Restart the MCP server to
pick up blocklist changes.

---

## Installation & Setup

### Requirements
Expand Down
45 changes: 45 additions & 0 deletions examples/pii_blocklist.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Example PII blocklist for the MariaDB MCP server.
#
# Activate by setting these two environment variables for the MCP server
# process:
#
# PII_FILTER_ENABLED=true
# PII_CONFIG_PATH=/absolute/path/to/your/pii_blocklist.yaml
#
# When the filter is enabled:
# - blocked columns are stripped from get_table_schema responses
# - fully-blocked tables (value "*") are hidden from list_tables
# - execute_sql refuses queries that would PROJECT a blocked column,
# including SELECT * over a table that has any blocked column
# - blocked columns are stripped from execute_sql result rows as a safety
# net (so the AI client never sees PII values even if a pre-flight check
# is bypassed by an unparseable query)
#
# Blocked columns are still allowed in WHERE / JOIN ON / GROUP BY / ORDER BY,
# because they do not produce PII in the response. The goal is to keep PII
# values from reaching the AI -- not to prevent a trusted human from filtering
# on them.
#
# Format
# ------
# Top-level keys are database names. Each value is a dict of table -> column
# list, or the literal string "*" to mark "every column of this table is PII."
# Names are matched case-insensitively.

acme_corp:
customers:
- email
- phone
- date_of_birth
- real_name
payment_methods:
- card_number
- cvv
- billing_address
# Use "*" to block every column. The table also disappears from list_tables.
audit_log_pii: "*"

acme_corp_reporting:
user_events:
- ip_address
- user_agent
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
[project]
name = "mariadb-server"
name = "mariadb-mcp"
version = "0.2.3"
description = "MariaDB MCP Server"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"asyncmy>=0.2.10",
"fastmcp[websockets]==2.12.1",
"google-genai>=1.15.0",
"openai>=1.78.1",
"python-dotenv>=1.1.0",
"pyyaml>=6.0",
"sentence-transformers>=4.1.0",
"sqlglot>=25.0",
"sshtunnel>=0.4.0",
"tokenizers==0.21.2",
]
13 changes: 13 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@
MCP_READ_ONLY = os.getenv("MCP_READ_ONLY", "true").lower() == "true"
MCP_MAX_POOL_SIZE = int(os.getenv("MCP_MAX_POOL_SIZE", 10))

# --- PII Filter Configuration ---
# When enabled, blocked columns are stripped from schema responses AND from
# result rows. The goal is to prevent the AI client from seeing PII values;
# end users with direct DB access are out of scope.
# When disabled, every PII filter function short-circuits in its first line —
# no YAML load, no SQL parsing, no per-row work.
PII_FILTER_ENABLED = os.getenv("PII_FILTER_ENABLED", "false").lower() == "true"
# Absolute path to the YAML blocklist file. Required when PII_FILTER_ENABLED=true.
PII_CONFIG_PATH = os.getenv("PII_CONFIG_PATH")

# --- Embedding Configuration ---
# Provider selection ('openai' or 'gemini' or 'huggingface')
EMBEDDING_PROVIDER = os.getenv("EMBEDDING_PROVIDER")
Expand Down Expand Up @@ -114,4 +124,7 @@
logger.info(f"No EMBEDDING_PROVIDER selected or it is set to None. Disabling embedding features.")

logger.info(f"Read-only mode: {MCP_READ_ONLY}")
logger.info(f"PII filter enabled: {PII_FILTER_ENABLED}")
if PII_FILTER_ENABLED and not PII_CONFIG_PATH:
logger.error("PII_FILTER_ENABLED=true but PII_CONFIG_PATH is not set. The filter will fail closed on first use.")
logger.info(f"Logging to console and to file: {LOG_FILE_PATH} (Level: {LOG_LEVEL}, MaxSize: {LOG_MAX_BYTES}B, Backups: {LOG_BACKUP_COUNT})")
Loading