pii-safe-db-mcp
You can safely explore and query a configured MySQL/PostgreSQL database while enforcing read-only policy checks and masking PII before results leave the server.
List accessible tables/views, optionally scoped to a schema/database.
Describe a table’s columns, types, PII classification, and whether columns are visible, masked, or blocked.
Run one read-only SQL statement (
SELECT,WITH,SHOW,DESCRIBE, orEXPLAIN) with bind parameters; results are capped byMAX_ROWS(default 200).Get sensitive-column and detected-PII values masked with stable tokens or configured partial/redact modes, while still allowing token-based grouping/counting/joining on returned output.
Rely on policy enforcement: table allow/deny lists, sensitive-column restrictions in filters/joins/grouping/sorting, AST taint tracking, fail-closed parsing, read-only transactions, query timeouts, sanitized errors, and audit logs.
Use
pii_scanto sample a table and report PII found by column name or value as counts only, never raw values.Configure custom PII patterns, masking behavior, audit logging, and policy via environment variables or a policy file.
Allows read-only querying of MySQL databases with automatic PII masking before results are returned, including table listing, schema inspection, masked query execution, and PII scanning.
Allows read-only querying of PostgreSQL databases with automatic PII masking before results are returned, including table listing, schema inspection, masked query execution, and PII scanning.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@pii-safe-db-mcp show me the 10 most recent customers with their emails and phone numbers"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
pii-safe-db-mcp
An MCP server that lets an LLM query MySQL or PostgreSQL while masking PII before any value leaves your machine. It also controls how sensitive columns may be used in a query.
Most database MCP servers forward the raw result set straight to the model. This one sits in between:
LLM ──► pii-safe-db-mcp ──────────────────────────────────► MySQL / Postgres
│
├─ 1. static read-only guard (single statement, no writes/files/sleep)
├─ 2. SQL AST policy check
│ • table allow / deny list (joins, subqueries, CTEs too)
│ • sensitive columns in WHERE / JOIN / GROUP BY / ORDER BY
│ • taint tracking through aliases, CTEs, derived tables, expressions
├─ 3. execution in a READ ONLY transaction + timeout
├─ 4. result masking (column origin + AST taint + value scanning)
├─ 5. error-message scrubbing
└─ 6. audit log (no raw PII, ever)Why masking the output isn't enough
SELECT c.customer_id, c.name
FROM customers c JOIN loans l ON c.pan = l.pan
WHERE c.customer_id = 123;Every value this query returns can be masked, yet it still uses the customer's PAN to link two datasets. Filters work the same way. WHERE email LIKE 'a%', ORDER BY name and CASE WHEN pan = 'X' act as oracles: they let a model reconstruct values one character at a time without ever seeing them. pii-safe-db-mcp parses every query into an AST and rejects these uses according to your policy.
Related MCP server: @guardbee/mcp-db-gateway
Features
MySQL and PostgreSQL in one server
Read-only, enforced three ways: a static SQL guard, a
READ ONLYtransaction, and a statement timeoutSQL AST security (v0.2): per-table rules for sensitive columns in
WHERE/HAVING,JOIN ON/USING,GROUP BY/PARTITION BYandORDER BY, including positional (ORDER BY 1) and alias referencesTaint tracking (v0.2): sensitive columns are followed through aliases, CTEs, derived tables, scalar subqueries,
UNIONs and functions.UPPER(name) AS xandSUBSTR(pan, 1, 3)are still masked.Table allowlist and denylist (v0.2): enforced for every table a query touches, including joins, subqueries and CTEs, and also applied to
list_tables,describe_table,pii_scan,DESCRIBEandSHOWFail-closed (v0.2): when a policy is configured, any query that can't be parsed and verified is rejected
Error sanitisation (v0.2): database errors that echo values (
Duplicate entry 'Ravi Kumar'…,Key (email)=(…)) are scrubbedAudit logging (v0.2): each record holds actor, environment, database, operation, decision, duration, row count, PII count and a literal-free query fingerprint. It never contains raw PII.
Indian PII recognisers (v0.2): PAN, Aadhaar (Verhoeff-checked), GSTIN, IFSC, UPI IDs, bank account numbers (context-anchored) and mobile numbers, plus custom patterns such as loan IDs
Alias-proof column detection:
SELECT email AS xis still masked, because the server uses the driver's column-origin metadata and the ASTValue scanning: PII hidden in
notes,commentsor JSON columns is masked inlineLLM-friendly tokens: the same value always maps to the same token (
<EMAIL_75423bc6b9>), so the model can still join on output, count distinct and compareSecrets are never oracles: password, token, OTP and API-key columns can't be filtered, joined, grouped or sorted on, even without a policy file
Tools
Tool | What it does |
| Lists the tables and views this server is allowed to access |
| Shows columns and types, each column's PII class and policy ( |
| Runs one read-only SQL statement, checked against the policy, and returns masked rows (capped at |
| Samples rows and reports PII found by column name and by value, as counts only |
Quick start
Claude Desktop / Claude Code
{
"mcpServers": {
"safe-db": {
"command": "npx",
"args": ["-y", "pii-safe-db-mcp"],
"env": {
"DATABASE_URL": "postgres://readonly_user:pass@localhost:5432/customer_db",
"PII_POLICY_FILE": "/path/to/pii-policy.json",
"AUDIT_LOG": "/var/log/pii-safe-db-mcp/audit.jsonl",
"AUDIT_ENVIRONMENT": "preprod"
}
}
}
}Claude Code one-liner:
claude mcp add safe-db \
-e DATABASE_URL=mysql://readonly:pass@localhost:3306/customer_db \
-e PII_POLICY_FILE=$PWD/pii-policy.json \
-- npx -y pii-safe-db-mcpPolicy file
The policy file is optional. Without one, the server behaves as v0.1 did: masking only, except that secret columns are never usable as oracles. See examples/pii-policy.json for a complete example.
{
"allowed_tables": ["customers", "loans", "public.payments"],
"denied_tables": ["internal_audit"],
"default": {
"allow_filtering": true,
"allow_joining": true,
"allow_grouping": true,
"allow_sorting": true
},
"tables": {
"customers": {
"sensitive_columns": ["name", "mobile", "pan", "aadhaar", "email"],
"blocked_columns": ["kyc_document_url"],
"allow_filtering": false,
"allow_joining": false,
"allow_grouping": false,
"allow_sorting": false
},
"loans": {
"sensitive_columns": ["pan"],
"allow_joining": false
}
},
"custom_patterns": [
{ "name": "LOAN_ID", "pattern": "\\bLN\\d{8}\\b", "column_names": ["loan_no"] }
],
"fail_closed": true,
"treat_detected_pii_as_sensitive": true
}Key | Meaning |
| If set, only these tables can be touched anywhere in a query. Accepts |
| These tables can never be touched. The denylist wins over the allowlist. |
| Columns that are always masked in output and subject to the clause rules below. Use this for PII that name detection can't see, such as |
| Columns that can't be referenced at all, and can't come back through |
| Set to |
| Allows |
| Allows |
| Allows |
| Allows |
| Clause rules for sensitive columns in tables that have no entry, and for auto-detected PII such as |
| Applies clause rules to columns detected as PII by name. Default |
| Extra value patterns (regex) and the column names that carry them. Masked as |
| Rejects queries the parser can't verify. Defaults to |
Always allowed: COUNT(col) and COUNT(DISTINCT col) on sensitive columns, because they reveal no values.
Always force-masked: any output expression derived from a sensitive column. If a sensitive expression is combined with SELECT *, it must have an alias.
Audit log
By default, one JSON line per tool call is written to stderr. Set AUDIT_LOG to a file path to append to a file (mode 600), or to off to disable it.
{
"timestamp": "2026-11-07T09:23:57.249Z",
"actor": "claude-code",
"environment": "preprod",
"database": "customer_db",
"db_type": "mysql",
"tool": "query",
"operation": "SELECT",
"decision": "ALLOWED",
"tables": ["customers", "loans"],
"duration_ms": 12,
"rows_returned": 10,
"pii_detected": 3,
"masked_columns": ["name", "mobile"],
"query_fingerprint": "ef4144c6cfcee85f",
"query_shape": "SELECT name, mobile FROM customers WHERE id = ?",
"raw_pii_logged": false
}decisionisALLOWED,BLOCKED(policy or guard) orERROR(database error, sanitised).actorcomes fromAUDIT_ACTOR. If that isn't set, the MCP client's own name is used (e.g.claude-code).query_shapehas every literal replaced by?, so it never contains values. Disable it withAUDIT_QUERY_SHAPE=false. Bind parameters are never logged.
Configuration
Env var | Default | Description |
| – |
|
| inferred |
|
| – | Alternative to |
|
| Enables TLS, with certificate verification |
| – | Path to the JSON policy file (see above) |
| – | The policy as an inline JSON string, instead of a file |
| – | Comma-separated shortcut for the allow and deny lists |
| auto | Forces fail-closed on ( |
|
|
|
| random per run | HMAC secret for tokens. Set it to keep tokens stable across restarts |
| – | Comma list of columns that can never be queried in any table |
| – | Columns that are always fully redacted: |
| – | Columns that are never masked, for false positives: |
|
| Scans text values for embedded PII |
|
| Maximum number of rows returned per query |
|
| Per-query timeout |
|
|
|
| MCP client name | Actor recorded in audit records |
|
| Environment label, e.g. |
|
| Includes the literal-free query shape in audit records |
Examples
-- ✅ allowed, output masked
SELECT c.id, UPPER(c.full_name) AS nm, l.amount
FROM customers c JOIN loans l ON c.id = l.customer_id ORDER BY l.amount DESC
-- → [{"id":1,"nm":"<NAME_ac0d9937c4>","amount":500000}, ...]
-- ✅ aggregates over sensitive columns
SELECT city, COUNT(DISTINCT email) AS n FROM customers GROUP BY city
-- ❌ Blocked: Column customers.pan is sensitive and cannot be used in JOIN conditions
SELECT c.name FROM customers c JOIN loans l ON c.pan = l.pan
-- ❌ Blocked: Column customers.email ... cannot be used in WHERE/HAVING filters or comparisons
SELECT id FROM customers WHERE email LIKE 'r%'
-- ❌ Blocked: Access to table "salaries" is not allowed by policy.
SELECT * FROM customers c WHERE EXISTS (SELECT 1 FROM salaries s WHERE s.id = c.id)Detected PII
Detected by column name (snake_case and camelCase):
Category | Columns |
Contact and identity | email, phone/mobile, first/last/full/borrower name, address, zip/pincode, DOB |
Government IDs | Aadhaar, PAN, GSTIN, passport, SSN |
Financial | bank account/IBAN, IFSC, UPI/VPA, card number, CVV |
Network | IP address |
Secrets | password, token, secret, api_key, otp, pin, mpin |
Detected by value: emails, UPI IDs, phone numbers (international and Indian mobile), credit cards (Luhn-checked), Aadhaar (Verhoeff-checked), PAN, GSTIN, IFSC, bank accounts next to an "A/C No" or "account" label, US SSN, IPv4, and your custom patterns.
Security notes and limitations
Use a read-only database user. The SQL guard, AST policy and read-only transaction are defence in depth. They are not a replacement for least privilege. For the strongest guarantee, also expose only a curated schema or views to that user.
Name detection and value patterns are heuristics. Run
pii_scan, then list any extra columns undertables.<t>.sensitive_columns.Value scanning can't detect free-text names or addresses. Add those columns to the policy.
Configure a policy for strict guarantees. With a policy, the server fails closed on SQL it can't parse. Without one, unparseable SQL falls back to masking only.
JSON paths are covered by value scanning, not the AST. In
data->>'pan', the output is masked by the PAN pattern. If a whole JSON column is sensitive, list it insensitive_columns.
Development
npm install
npm test # builds, then runs unit, SQL-bypass and end-to-end regression tests
npm start # runs the server on stdio (needs DATABASE_URL)The test suites:
File | Covers |
| Detection and masking |
| SQL bypass attempts in both dialects, Indian recognisers, error sanitisation, audit |
| Runs the MCP server in memory, calls every tool and error path, and asserts that no raw PII appears in any response or audit record |
License
MIT © Aqib Siddiqui
Available Tools
4 toolsdescribe_tableDescribe tableARead-only
Show the columns of a table, their types, and whether each column is classified as PII (and will be masked) or blocked.
Inputs: table (required).
list_tablesList tablesARead-only
List tables and views in the connected postgres database.
Inputs: schema.
pii_scanScan table for PIIARead-only
Sample rows from a table and report which columns contain PII (by column name and by detected values). Returns counts only, never values. Use it to discover PII hiding in free-text columns.
Inputs: table (required), sample_size.
queryRun read-only SQLARead-only
Run a single read-only SQL statement (SELECT / WITH / SHOW / DESCRIBE / EXPLAIN) against postgres. Results are capped at 200 rows. PII is masked before results are returned: columns recognised as PII (email, phone, name, address, Aadhaar, PAN, card numbers, passwords...) are replaced with stable tokens like <EMAIL_3f9a1c2b7d> (same input → same token, so you can still GROUP BY / COUNT DISTINCT / compare), and PII patterns found inside other text columns are masked too. Prefer aggregates (COUNT, GROUP BY) over pulling raw rows. Use ? (MySQL) or $1 (Postgres) placeholders with params.
Inputs: sql (required), params.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 4 tools
Each tool has a fairly clear role: schema listing, schema inspection, ad-hoc SQL execution, and PII value sampling. The main soft overlap is between describe_table (declared/classified PII) and pii_scan (PII detected in sampled values), which could confuse an agent deciding how to audit a column, but the descriptions do distinguish name-based vs value-based detection.
list_tables and describe_table follow a clean verb_noun snake_case pattern, but 'query' is a bare verb with no object and 'pii_scan' reverses the order into noun_verb. Readable overall, but the convention is not uniform across the set.
Four tools is lean but defensible for a deliberately read-only, PII-safe database interface where each tool maps to a distinct stage (discover tables, inspect columns, execute, audit PII). It sits just below the ideal range, and a schema-search or relationship tool could plausibly earn a place.
The read-only domain is well covered: discovery, column classification, masked querying with parameters, and PII detection in free text. Gaps are minor and mostly by design (no foreign-key/relationship introspection, hard 200-row cap with no pagination), and agents can work around them via query.
Maintenance
Related MCP Connectors
Safe, read-only Postgres and MySQL access for AI agents. Audit log + column-level controls.
Guard AI agents' PostgreSQL/MySQL access via MCP: SQL audit, auth, masking, write approval
Query 40 databases from Claude, ChatGPT, or Cursor — on any device. Read-only, encrypted, audited.
Governed SQL access to your databases: redaction, query review and an audit log.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceProvides read-only access to PostgreSQL databases via MCP, enforcing least-privilege roles, row-level security, masked views, and SQL AST guardrails to prevent data leakage and unauthorized operations, enabling AI agents to safely query sensitive production data.MIT

@guardbee/mcp-db-gatewayofficial
AlicenseAqualityFmaintenanceEnables LLMs to safely query databases through a secure gateway that masks PII, enforces role-based table access, rate limits requests, and logs all queries, with Prisma support.4762 npmMIT- AlicenseAqualityBmaintenanceEnables AI agents to query PostgreSQL databases securely with read-only defaults, PII masking, rate limiting, and human-approved, expiring mutation tokens.91Apache 2.0
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to query a Postgres database read-only through a policy layer rather than a raw connection string. Enforces table and column allowlists, row limits, mandatory predicates, PII masking and append-only audit logging, while refusing writes, joins, subqueries and unlisted functions.MIT