mcp-postgres-guard
An MCP server that enables AI agents to securely interact with PostgreSQL databases with least-privilege scopes, PII masking, and human approval for writes.
README
mcp-postgres-guard
An MCP server that gives AI agents access to a PostgreSQL database without giving them the keys to it: least-privilege scopes, statement analysis by a real parser, PII masking, human approval for every write, and an append-only audit trail.
Works with Claude Desktop, Claude Code, or any MCP client.
// claude_desktop_config.json
{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "mcp-postgres-guard"],
"env": {
"DATABASE_URL": "postgres://readonly@localhost:5432/app",
"READABLE_TABLES": "customers,orders",
"MASKED_COLUMNS": "ssn,customers.email"
}
}
}
}
That configuration is read-only with two tables visible and two columns redacted. Writes require three more variables, deliberately.
The problem
Handing an agent a database connection means handing it everything the connection can do. The usual mitigations do not hold up:
| Mitigation | How it fails |
|---|---|
| "The prompt says read-only" | Prompts are advisory. A confused agent, or an injected instruction inside a row of data, ignores them |
Regex check for DROP/DELETE |
Defeated by DR/**/OP, by a keyword inside a string literal, by casing, by SELECT 1; DROP TABLE users |
| A read-only database role | Correct and necessary, but all-or-nothing: it cannot express "may update orders, never customers", nor mask a column, nor ask a human |
| Reviewing the agent's SQL afterwards | The row is already gone |
This server addresses each layer separately, so no single check has to be perfect.
What it does
Statements are parsed, not pattern-matched
Every statement goes through a real PostgreSQL parser
before it reaches the database. That yields a structural answer — statement
class, every table touched, whether a mutation carries a WHERE clause —
instead of a guess.
SELECT 1; DROP TABLE users
→ Expected a single statement, found 2. Stacked statements are refused.
SELECT * FROM logs WHERE message = 'DROP TABLE users'
→ allowed. It is a read; the keyword is data.
The second case matters as much as the first: a guard that produces false positives gets switched off.
Reads cannot write, twice over
A statement classified as a read still executes inside BEGIN READ ONLY. If
the analyser ever misjudges a statement class, Postgres refuses the write on its
own. Defence that does not depend on this codebase being correct is the only
kind worth relying on here.
search_path is pinned to the exposed schemas, so an unqualified table name
cannot resolve to something the policy check never saw.
Columns can be masked without being hidden
SELECT id, name, email, ssn FROM customers
[{ "id": 1, "name": "Ann Lee", "email": "***REDACTED***", "ssn": "***REDACTED***" }]
The agent still sees that email exists and can filter on it — queries stay
valid — but the values never leave the process. Masking is applied to result
rows rather than rewritten into the query, because a SELECT can surface a
column under an alias, through *, or via an expression; output filtering
catches all three.
Writes are measured, then approved by a human
A mutation is first executed inside a transaction that is always rolled back, purely to learn how many rows it really touches. Only then is a human asked, through MCP elicitation:
Approve this UPDATE?
Reason given: Customer confirmed delivery address
Rows affected: 1
UPDATE orders SET status = $1 WHERE id = $2
The count comes from an actual execution rather than EXPLAIN, because planner
estimates are least reliable on stale statistics — exactly the situation where
an approval prompt earns its keep.
UPDATE/DELETE with no WHERE clause is refused outright rather than sent
for approval. A human scanning a wall of SQL misses a missing WHERE
reliably, and it is the one mistake with no undo.
If the client does not support elicitation, the write is declined. "Nobody could be asked" is not "approved".
Everything is recorded
Every call — allowed, denied, approved, declined, failed — is appended as JSONL with the SQL, the tables, the row count, and the reason:
{"timestamp":"2026-07-28T14:03:11.204Z","tool":"query","outcome":"denied",
"sql":"SELECT * FROM internal_secrets",
"reason":"Table \"internal_secrets\" is not in the readable set."}
Denied calls are logged as carefully as successful ones. A log of successful queries alone cannot tell a well-behaved agent from one that probed for a way in and was stopped.
Tools
| Tool | Description |
|---|---|
list_tables |
Tables readable under the policy, with row estimates |
describe_table |
Columns, types, nullability, defaults; masked columns marked |
query |
A single SELECT, read-only transaction, row-limited |
execute |
A single INSERT/UPDATE/DELETE, dry-run then human approval |
Denials return the reason to the agent. An agent told only "denied" retries the
same query; one told "table secrets is not in the readable set" moves on.
Configuration
| Variable | Default | Purpose |
|---|---|---|
DATABASE_URL |
— | Required |
ALLOWED_SCHEMAS |
public |
Schemas visible at all |
READABLE_TABLES |
(all) | Readable tables; empty means every table in the allowed schemas |
WRITABLE_TABLES |
(none) | Writable tables; writes are opt-in |
MASKED_COLUMNS |
(none) | ssn or customers.email |
WRITES_ENABLED |
false |
Master switch for mutations |
REQUIRE_APPROVAL |
true |
Human confirmation per write |
MAX_MUTATION_ROWS |
100 |
Refuse a mutation above this blast radius |
MAX_ROWS |
200 |
Read result cap |
STATEMENT_TIMEOUT_MS |
10000 |
Server-enforced |
MAX_CALLS_PER_MINUTE |
60 |
Bounds an agent stuck in a retry loop |
AUDIT_LOG_PATH |
(stderr) | JSONL audit file |
Two configurations are rejected at startup rather than failing confusingly later: writes enabled with no writable tables named, and a table that is writable but not readable — a change that cannot be reviewed cannot be approved.
Try it
npm install
docker run -d --name guard-demo -e POSTGRES_PASSWORD=pw -p 5432:5432 postgres:17
DEMO_DATABASE_URL=postgres://postgres:pw@localhost:5432/postgres \
node --experimental-strip-types scripts/demo.ts
scripts/demo.ts connects as a real MCP client and walks through reads,
masking, six kinds of denial, and an approved write — printing exactly what a
connected agent would see.
Or point the MCP Inspector at it:
npm run inspect
Testing
npm test # 64 tests
npm run typecheck
The test suite concentrates on the security boundary, because that is where a
bug is expensive rather than annoying: stacked-statement rejection, keywords
inside string literals, comment-split keywords, CTE names mistaken for tables,
forbidden tables reached through a subquery, writes whose FROM clause reads a
table the agent may not see, and masking applied through aliases.
scripts/demo.ts covers the protocol layer itself — that tools are registered
with the schemas MCP expects, that denials come back as tool errors rather than
crashes, and that declining an approval leaves the data untouched.
Deliberately not included
- Query rewriting — injecting predicates for row-level security is attractive and fragile. Postgres RLS does it properly; use it.
- Connection pooling per agent identity — belongs in the deployment, not here.
- Automatic PII detection — heuristics that guess which columns are sensitive are wrong in both directions. Name them explicitly.
License
MIT
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。