Skip to main content
Glama
vladshykanov

mcp-postgres-guard

by vladshykanov

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.


Related MCP server: AgenticMCP

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

Available Tools

4 tools
describe_tableDescribe tableA
Read-onlyIdempotent

Columns, types, nullability and defaults for one table. Masked columns are marked — they can be filtered on, but their values are never returned.

ParametersJSON Schema
NameRequiredDescriptionDefault
tableYesTable name, optionally schema-qualified (e.g. public.users)

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint and idempotentHint. The description adds that masked columns are marked and can be filtered but never returned, which is useful behavioral context beyond the schema.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences with no wasted words. The first sentence states the core purpose, the second adds an important detail about masked columns.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description is complete for a simple schema inspection tool: it explains what is returned (columns, types, nullability, defaults, masked marks) given one parameter. However, it could mention the return format (list of columns) explicitly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents the parameter. The description does not add new information beyond what the schema provides (table name, optionally schema-qualified).

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool returns columns, types, nullability, and defaults for a single table, which matches the tool name and distinguishes it from siblings like list_tables (lists tables) and query (runs queries).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies the tool is for inspecting table schema, but it does not explicitly state when to use it over siblings like list_tables or query, nor does it provide when-not-to-use guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

executeExecute a writeA
Destructive

Runs a single INSERT, UPDATE or DELETE. The change is first executed in a rolled-back transaction to measure exactly how many rows it affects, then that number is shown to a human for approval. UPDATE/DELETE without a WHERE clause is always refused.

ParametersJSON Schema
NameRequiredDescriptionDefault
sqlYesA single INSERT, UPDATE or DELETE statement
paramsNoValues for $1, $2 … placeholders
reasonYesWhy this change is needed — shown to the approver

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description adds significant behavioral context beyond annotations (readOnlyHint=false, destructiveHint=true): it explains the rolled-back transaction to measure row count, human approval step, and refusal of UPDATE/DELETE without WHERE. This fully discloses safety mechanisms.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, no wasted words. The first sentence states the primary purpose, the second elaborates on behavior. Front-loaded and efficient.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity of a write tool with safety concerns, the description explains the dry-run and approval flow. No output schema exists, but the description mentions row count measurement. Not fully explicit about return format, but adequate for the intended usage.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3. The description does not add substantial meaning beyond the schema; it reinforces that 'reason' is shown to the approver, but this is already in the schema. No additional parameter guidance.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool runs a single INSERT, UPDATE, or DELETE statement, specifying the verb and resource. It distinguishes from siblings (list_tables, describe_table, query) by focusing on write operations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context for when to use this tool (single write statements) and notes that UPDATE/DELETE without a WHERE clause is refused. However, it does not explicitly name alternatives or state when not to use, though siblings imply read operations.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_tablesList tablesA
Read-onlyIdempotent

Lists tables the agent is permitted to read, with row estimates. Start here — guessing table names wastes calls against the rate limit.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already provide readOnlyHint and idempotentHint. Description adds value by stating it returns row estimates and mentioning rate limit context, beyond what annotations convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two concise sentences with no wasted words. Front-loaded with key action and followed by usage guidance. Every sentence earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given zero parameters and no output schema, description fully informs the agent: what tool returns, when to use, and why it matters. Sibling tools are distinct and covered.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

No parameters exist, so baseline is 4. Description implicitly tells agent no input needed, which adds clarity.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description uses specific verb 'Lists' and resource 'tables' with additional detail 'the agent is permitted to read, with row estimates'. Clearly distinguishes from siblings like describe_table which describes a specific table.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly tells agent to 'Start here' and warns against guessing table names due to rate limits. Provides clear when-to-use guidance and alternatives (guessing is bad).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

queryRun a read-only queryA
Read-only

Executes a single SELECT inside a read-only transaction, capped at 200 rows. Use $1, $2 … placeholders with the params array — never string-concatenate values into the SQL.

ParametersJSON Schema
NameRequiredDescriptionDefault
sqlYesA single SELECT statement
paramsNoValues for $1, $2 … placeholders

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Description adds row cap and security guidance beyond the readOnlyHint annotation. No contradictions. The row limit is critical behavioral info not in annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two concise sentences, each essential. Front-loaded with key action and constraints. No wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple 2-param tool with no output schema, description covers purpose, constraints, and safe usage. Could mention return format, but implied by SQL execution.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema covers both parameters with descriptions. Description adds anti-pattern warning and clarifies parameter roles (placeholders for $1, $2). Adds meaningful value beyond schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states it executes a single SELECT in a read-only transaction with a 200-row cap. Distinguishes from siblings like execute (writes) and list_tables/describe_table (metadata).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit guidance on using placeholders and warns against string concatenation. Does not explicitly mention when not to use, but read-only hint and sibling context imply appropriate use cases.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

  1. 4 tool updatesv0.1.0
    • First observeddescribe_table
    • First observedexecute
    • First observedlist_tables
    • First observedquery

TDQS

A4.4/5.0
Disambiguation5/5

Each tool targets a distinct action: listing tables, describing schema, executing read queries, and performing safe mutations. There is no overlap in functionality.

Naming Consistency5/5

All tool names follow a consistent snake_case verb_noun pattern: list_tables, describe_table, query, execute. The naming is predictable and clear.

Tool Count5/5

With 4 tools, the server is well-scoped for its purpose of providing safe database access. Each tool plays a necessary role without redundancy.

Completeness4/5

Covers essential operations: schema exploration, reading, and safe writes. Minor gap for multi-statement transactions, but the guard design prioritizes safety.

Maintenance

ActivitySlowing
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    An MCP server that gives an AI agent scoped, safe access to your Postgres databases with per-connection access control, row caps, timeouts, and defense-in-depth read-only enforcement.
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    A Model Context Protocol (MCP) server that provides secure, role-based access to PostgreSQL databases for AI agents.
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    An MCP server that gives AI agents access to configured databases (PostgreSQL, MySQL, Redshift, SQL Server) with SSH/AWS SSM tunnels, pluggable secret providers, and strict per-instance isolation.
    10
    79
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/vladshykanov/mcp-postgres-guard'

If you have feedback or need assistance with the MCP directory API, please join our Discord server