Skip to main content
Glama
PgGuard Agent

PgGuard Agent

by vk-ai

PgGuard Agent

Corporate-grade Postgres governance MCP server for Cursor and other MCP clients.

PgGuard sits between an AI assistant and your PostgreSQL database. It enforces role-based access (RBAC), statement classification, confirmation gates for DML/DDL, optional HashiCorp Vault secrets, and append-only audit logging — so agents can query and change data without bypassing corporate controls.

Repo: github.com/vk-ai/pg-governance-agent · License: MIT


Docs

Doc

What it is for

ARCHITECTURE.md

Components, data flow, MCP surface

SECURITY.md

Threat model, secrets, confirm gates

GOVERNANCE.md

Roles, policy.yaml, audit expectations

LOCAL_TEST.md

Rancher Desktop / Compose local walkthrough


Related MCP server: Postgres MCP Server

What you get

MCP tools

Tool

Purpose

db_health

Ping + Postgres version

list_schemas / list_tables / describe_table

Safe introspection, limited to the role's allowed_schemas

run_select

SELECT only; allowed schemas only; capped by rows, bytes and cell size (truncated: true when a cap applied)

run_dml

Data changes — requires confirm: true plus a human-signed approval (approval_id / approval_token) when policy requires it; dry_run: true previews the affected row count (no approval needed); capped by max_affected_rows

run_ddl

Schema changes — requires confirm plus an approval when policy requires it; optional dry_run

request_approval

Queue one exact DML/DDL statement for a human approver (returns approval_id + SQL sha256; DML includes a dry-run row count)

explain_query

EXPLAIN (ANALYZE gated by policy)

get_audit_tail

Recent hash-chained audit events (not granted to reader by default since phase 5)

whoami

Effective role + policy (+ optional JWT principal)

Principal propagation — HS256 JWT, verified and required by default, (bearer_token / PGGUARD_BEARER_TOKEN) maps to Postgres session GUCs (app.user_id, …) for RLS. See SECURITY.md.

Roles (PGGUARD_ROLE) — see config/policy.yaml

Role

Typical use

DB user (default policy)

reader

Analyst / read-only agent

app_reader

writer

App DML with confirmation

app_writer

migrator

Schema changes with confirmation

app_migrator

admin

Break-glass (still audited)

per policy


Requirements

  • Node.js 20+

  • A reachable PostgreSQL instance (existing corporate DB or the demo Compose stack)

  • Optional: HashiCorp Vault KV v2 for credentials (PGGUARD_SECRETS_PROVIDER=vault)


Onboard with an existing Postgres DB

Use this path when you already have Postgres (local, RDS, Cloud SQL, on-prem, etc.). You do not need Docker Compose.

1. Clone and install

git clone https://github.com/vk-ai/pg-governance-agent.git
cd pg-governance-agent
cp .env.example .env
npm install
npm test
npm run policy:check
npm run build

Map users to roles in config/policy.yaml (db_user per role). Example grants (adjust schema names):

-- Read-only
CREATE ROLE app_reader LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE your_db TO app_reader;
GRANT USAGE ON SCHEMA public TO app_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_reader;

-- Writer (DML)
CREATE ROLE app_writer LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE your_db TO app_writer;
GRANT USAGE ON SCHEMA public TO app_writer;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_writer;

-- Migrator (DDL) — tighten to your change process
CREATE ROLE app_migrator LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE your_db TO app_migrator;
GRANT ALL ON SCHEMA public TO app_migrator;

You can point a single role at an existing service account by editing db_user in config/policy.yaml — prefer dedicated agent roles over shared superusers.

Then apply DB-native hardenings (timeouts, read-only default for readers, NOBYPASSRLS, lock down public):

psql "$DATABASE_URL" -f scripts/provision-roles.sql

Compose demos run the same SQL via docker/postgres/init/04-agent-roles.sql on first boot. The MCP server refuses to start if preflight finds a superuser/BYPASSRLS role, statement_timeout=0, or dblink/postgres_fdw installed.

3. Point .env at your database

Set at least:

PGGUARD_ROLE=reader
PGGUARD_SECRETS_PROVIDER=env
PGGUARD_POLICY_PATH=./config/policy.yaml
PGGUARD_AUDIT_DIR=./data/audit

PGHOST=your-db-host.example.com   # prefer 127.0.0.1 for local sockets/quirks
PGPORT=5432
PGDATABASE=your_db
PGUSER=app_reader
PGPASSWORD=...                    # never commit .env
PGSSLMODE=verify-full             # required for non-loopback hosts (phase 5 network policy)
PGSSLROOTCERT=/etc/pgguard/db-ca.pem  # CA that signed the DB server certificate

Add the DB host name to network.allowed_db_hosts in config/policy.yaml (see "Network allowlist, TLS, tamper-evident audit and CI (phase 5)" below), or PgGuard refuses to connect.

Vault instead of env passwords: set PGGUARD_SECRETS_PROVIDER=vault, VAULT_ADDR, VAULT_TOKEN (or AppRole), and VAULT_KV_PATH. Store keys such as host, port, database, user, password, sslmode (and optional reader_user / writer_user / …) under that path. Details in SECURITY.md.

4. Wire Cursor MCP

  1. Copy config/cursor-mcp.example.json.

  2. Replace REPLACE_WITH_ABS_PATH with the absolute path to this repo.

  3. Set PG* (or Vault) env to your database — not the demo corpdb values.

  4. Paste into Cursor MCP settings and reload MCP servers.

Or run stdio directly:

npm run mcp

5. Smoke-test tools

  1. whoami → reader (or your chosen role)

  2. db_health → version string

  3. list_tables for your schema

  4. run_select → limited rows

  5. run_dml as reader → denied; as writer with confirm: true → allowed

  6. get_audit_tail (writer and up; reader no longer has audit_read) → events under data/audit/; npm run audit:verify checks the chain

Tune allowlists, max_rows, and confirm flags in config/policy.yaml. Governance expectations: GOVERNANCE.md.


Demo stack (no existing DB)

If you want a disposable local Postgres + Vault (Rancher Desktop / Docker):

cp .env.example .env
npm install && npm test && npm run policy:check
docker compose up -d          # postgres:17 + vault + vault-init
npm run build && npm run mcp

Seeded demo DB: corpdb, schema corp (orgs / employees). Step-by-step: LOCAL_TEST.md.

Demo passwords live only in .env.example and Compose init scripts. Never commit .env.


Package scripts

Script

What it does

npm run build

Compile TypeScript → dist/

npm run mcp

Start stdio MCP server

npm test

Unit tests (+ live Postgres tests when PGGUARD_IT_DATABASE_URL is set)

npm run policy:check

Validate config/policy.yaml

npm run approvals -- <cmd>

Approver CLI: keygen, list, show, approve, deny (needs the private key; run it outside the server's environment)

npm run audit:verify -- [file] [--expect-head seq:hash]

Check the audit hash chain; prints the first bad line and the current head


Read-only transactions (let Postgres say no)

Read tools (db_health, list_*, describe_table, run_select, explain_query) run as:

BEGIN READ ONLY
  set_config('statement_timeout' | 'lock_timeout' | 'idle_in_transaction_session_timeout', <policy>, true)
  set_config('app.user_id', <jwt sub>, true)   -- principal GUCs, same transaction
  <your SELECT>                                  -- extended protocol: one statement only
ROLLBACK
  • The classifier is the first gate. Postgres is the second: a SELECT that calls a writing function, or EXPLAIN ANALYZE of DML, fails with cannot execute … in a read-only transaction.

  • Timeouts come from defaults in config/policy.yaml (statement_timeout_ms, lock_timeout_ms, idle_in_transaction_session_timeout_ms; per-role overrides; 0 disables via SET LOCAL). Role-level ALTER ROLE … SET statement_timeout (phase 2) is the backstop when the app does not set them.

  • DDL (run_ddl / withClient) now gets the same policy timeouts.

  • Principal GUCs are now set inside the transaction that runs the query. Before, set_config(..., true) ran on its own, outside any transaction, so the value was gone before the query ran. run_dml/run_ddl with a principal run in BEGIN … COMMIT for the same reason.

  • The demo RLS policy (docker/postgres/optional/rls-orders-by-employee.sql) fails closed: no principal means no rows.

Try it against a throwaway database (needs a superuser URL; the Compose Postgres works):

docker compose up -d postgres
PGGUARD_IT_DATABASE_URL=postgres://postgres:postgres_dev_only@localhost:5432/postgres npm test

Without PGGUARD_IT_DATABASE_URL, the live tests are skipped and only the unit tests run.


DML preview and row cap (run_dml dry_run + max_affected_rows)

Agents that run DELETE/UPDATE with a bad WHERE clause are a common horror story (HN: agent deleted the prod DB, HN discussion). run_dml now runs every statement as:

BEGIN
  set_config(<timeouts>, true); set_config('app.user_id', <jwt sub>, true)   -- same as reads
  <your INSERT/UPDATE/DELETE>          -- extended protocol: one statement only
  dry_run        -> ROLLBACK, return rowCount (+ up to 5 RETURNING rows)
  rowCount > cap -> ROLLBACK, return an error
  otherwise      -> COMMIT
  • dry_run: true needs no confirm. It returns rowCount, would_exceed_cap and a sample from a RETURNING clause. The count goes through the same RLS policy and principal as the real write, so it is the real count for that user at that moment.

  • max_affected_rows comes from config/policy.yaml (default 100; writer 50, migrator 1000, admin 10000). The cap is checked inside the write's own transaction, so there is no gap between "preview said 3 rows" and "the write hit 3000".

  • Writes now also get the policy's statement_timeout / lock_timeout.

Real vs not verified. Tested against a live Postgres 17 (9 integration tests: counts, RLS per principal and fail-closed, cap rollback, commit under the cap, WITH CHECK errors, clean pooled session) plus unit tests of the SQL order. Limits: a rolled-back statement still advances sequences, runs triggers (their writes are undone; anything they do outside the database is not), and holds row locks until the rollback, so a dry run on a hot table can block other writers for up to lock_timeout/statement_timeout. The count is a snapshot; it can differ when the real write runs later. DDL is unchanged (run_ddl dry_run still only parses). Since phase 5 these tests also run in CI against Postgres 17 (.github/workflows/ci.yml).


Out-of-band approvals (phase 4)

Learning project: this shows one way to make "a human approved this exact statement" something the gateway can check, instead of trusting the agent's confirm: true. It has not been used with a real change-management system.

Flow

  1. The agent calls request_approval with tool (run_dml / run_ddl) and the exact SQL. PgGuard classifies it, runs the schema guard, does a rolled-back dry run for DML, and writes data/approvals/pending/<approval_id>.json. It returns approval_id and the SQL's sha256.

  2. A human, in a separate session that holds the Ed25519 private key, reviews and approves:

    npm run approvals -- keygen ~/.pgguard-approver          # once; give the server approver.pub
    npm run approvals -- list
    npm run approvals -- show <approval_id>                   # SQL, hash, dry-run row count, reason
    PGGUARD_APPROVAL_PRIVATE_KEY_FILE=~/.pgguard-approver/approver.key \
      npm run approvals -- approve <approval_id> --approver alice --ttl 600

    (deny <approval_id> --approver alice --note "…" refuses it.)

  3. The agent retries run_dml / run_ddl with the same SQL, confirm: true and approval_id (or the printed approval_token).

What the server checks (src/approvals/token.ts, src/approvals/gate.ts), before the statement runs:

  • Signed out of band. The token is pga1.<claims>.<Ed25519 signature>. The server holds only the public key (PGGUARD_APPROVAL_PUBLIC_KEY or PGGUARD_APPROVAL_PUBLIC_KEY_FILE). It refuses to start if any PGGUARD_APPROVAL_PRIVATE_KEY* variable is in its environment.

  • Bound to the SQL. The token carries sha256 of the SQL exactly as submitted. Any byte change, including whitespace, needs a new approval. It is also bound to the tool, PgGuard role, DB user, database name and the JWT sub (null when there is no principal). All of these come from the live call, never from the token.

  • Expiring. exp - iat must be ≤ approvals.max_ttl_seconds (900 in the shipped policy). Expiry has no grace period; clock_skew_seconds (30) only tolerates an approver clock running ahead for iat.

  • Single use. Each token has a random 128-bit jti, recorded in a ledger:

    • ledger: file (default): data/approvals/used/<jti> created with O_EXCL, so two processes on one host cannot both use it. It is spent before the statement runs.

    • ledger: postgres: docker/postgres/optional/approval-ledger.sql installs pgguard.consume_approval(), a SECURITY DEFINER function writing to a table agent roles cannot read or write. For run_dml it runs inside the DML transaction, so the approval is spent only if the write commits. It also rejects a token whose exp has passed by the database clock.

  • Row cap. The approver can set --max-rows (DML defaults to the dry-run count they were shown). The run uses min(policy max_affected_rows, approved max).

  • No self-approval. approver must differ from the requesting sub.

  • Fail closed. With required_for: [dml, ddl] (shipped default) and no public key configured, real runs are refused. dry_run never needs an approval. whoami shows approvals.public_key_configured.

Tests: tests/phase4-approvals.test.ts (40 offline) and tests/phase4-approvals.integration.test.ts (8 live, Postgres 17): end to end request → approve → run once → replay refused; Postgres ledger replay; cap rollback leaves the approval unspent; approver cap of 0 rolls back a 1-row update; wrong-SQL and wrong-database tokens never reach the DB; agent roles get 42501 on the ledger table; the DB clock rejects an expired consume; DDL approved once; a writer's token refused for the migrator role.

What is real vs. not

  • Real: Ed25519 signing and verification (node:crypto, no new dependencies), SQL/scope binding, expiry, both ledgers, the CLI, live Postgres tests.

  • Not built: no Slack/email/ticket integration, no web UI, no multi-approver quorum, no key rotation or revocation list. The request queue is a local directory, so the agent host and the approver must share it, or the approver passes the token back by hand.

Limits (honest)

  • approver is a name the key holder types. Anyone with the private key can approve as anyone. One key per approver and a revocation list are not implemented.

  • The file ledger is per host. Two PgGuard hosts with separate data/ directories could each accept the same token once. Use ledger: postgres when more than one server can reach the database.

  • Burn then run: with the file ledger (and for all DDL), the approval is spent before the statement. If the statement then fails, a new approval is needed. DDL may run in autocommit, so it cannot share a transaction with the ledger.

  • An agent that calls pgguard.consume_approval() directly can burn approvals (deny itself). It cannot create one. The schema guard also blocks the pgguard schema for roles limited to corp.

  • The approver sees the SQL text and a dry-run count from one moment in time. Data can change before the approved run, and the row cap is the backstop for that.

  • Existing pre-phase-4 live suites (dml-preview, read-txn) set requiredFor: [] because they test caps and RLS, not approvals.

  • npm test now runs Jest with --runInBand. Live suites run ALTER ROLE (cluster-wide) in their setup, and in parallel one suite could reset another's statement_timeout (seen once in six parallel runs as a phase 2 timeout). In-band, the full suite took about 7 s locally.


Network allowlist, TLS, tamper-evident audit and CI (phase 5)

Network allowlist (network: in config/policy.yaml). Before any connection (every tool call and the startup preflight), PgGuard:

  1. checks the credential host against allowed_db_hosts: exact names, a leading *.suffix wildcard (one or more labels), IP literals or CIDRs. A name never matches an IP entry and an IP never matches a name entry. A host that isn't listed is refused before DNS is asked;

  2. resolves the name once and refuses link-local and unspecified addresses (169.254.0.0/16 including cloud metadata, fe80::/10, 0.0.0.0/8, ::) whatever the policy says. With allowed_db_cidrs set, every resolved address must be inside one of them;

  3. requires sslmode=verify-full unless plaintext_loopback_ok: true and every resolved address is loopback (the shipped local-demo setting; turn it off anywhere shared);

  4. connects to the address it just checked (pin_resolved_ip: true) and sends the original hostname as TLS SNI and for the certificate check, so a second DNS answer can't redirect the socket after the check.

PGSSLROOTCERT (or Vault sslrootcert) gives the CA bundle. Like libpq's sslrootcert, it replaces the default trust store. When sslmode=verify-full, preflight also checks pg_stat_ssl and refuses to start on an unencrypted session. No network: block means no allowlist, and whoami reports network.enforced: false. An empty allowed_db_hosts is an error, not "allow all".

Tamper-evident audit (audit:). Each line of data/audit/events.jsonl now has seq, prev, alg and hash = H(prev + "\n" + canonical JSON of the line). H is sha256, or HMAC-SHA256 when PGGUARD_AUDIT_HMAC_KEY (hex, 32 bytes or more) is set. Lines also carry sql_sha256 of the full SQL (the preview is still cut at 500 chars), and approval ids from phase 4 stay in detail. The server refuses to start if the existing log fails verification (verify_on_start: true). npm run audit:verify exits 1 at the first edited, removed, reordered or inserted line and prints the head {seq, hash}. Pass a previously recorded head with --expect-head seq:hash to detect a cut-off tail. Lines written before phase 5 are counted as legacy and aren't protected. The reader role no longer has audit_read, because the log holds other principals' SQL.

CI. .github/workflows/ci.yml runs typecheck, policy:check and the full suite on every PR and on pushes to main. It runs against a Postgres 17 container started with ssl=on, so the live TLS tests run too (PGGUARD_IT_REQUIRE_TLS=1 turns a TLS skip into a failure).

Real vs not. Tested against Postgres 17 on the box (TLS on, self-signed cert), with 32 new offline tests and 9 new live tests:

  • offline: allowlist matching and lookalike names, a host that isn't listed never reaches DNS, metadata and IPv4-mapped addresses, mixed good/bad DNS answers, sslmode rules, pinning against a rebinding resolver, policy validation, audit edit/delete/reorder/ insert/truncate/downgrade/restart/two writers, and the CLI exit codes;

  • live:

    • a name that exists only in the injected resolver still connects, which proves the pinned IP is used;

    • a reachable server under a name that isn't listed is refused;

    • verify-full connects over TLSv1.3 with the right CA and name;

    • the wrong name is rejected, and an unrelated CA bundle is rejected;

    • preflight ssl_in_use fails on a plaintext session;

    • an edited audit line from real tool calls is caught, and startup is refused.

Limits:

  • This is an application check, not egress control. A compromised process can ignore it. Real egress control is a NetworkPolicy or security group (deploy/networkpolicy.yaml) plus hostssl-only pg_hba.conf rules for the agent roles from the PgGuard subnet, and no dblink / postgres_fdw (preflight already refuses them).

  • allowed_db_cidrs is optional. Without it, a listed name that later resolves somewhere unexpected is still accepted, except for link-local and unspecified addresses.

  • Pinning uses the first resolved address; there is no failover across the other answers.

  • Without the HMAC key, anyone who can write the file can rebuild the whole chain. With the key on the same host, so can anyone who can read the key. Truncation is only detectable against a head recorded elsewhere. Ship lines and heads to an append-only store (SIEM, object lock).

  • One writer per audit file. A second process continues from the file tail before each append, but truly concurrent appends can fork the chain, and audit:verify would then flag it.

  • Verification on start reads the whole file (linear in log size). Rotate logs, and keep each rotated file's head.


Security notes (short)

Phase 1 security fixes (this is a learning project; the threat model and limits are below).

What changed, and why:

  • Cross-tenant read via set_config (fixed, reproduced live). Before, SELECT * FROM corp.orders WHERE set_config('app.user_id','2',true) IS NOT NULL let user 1 read user 2's rows, because the RLS policy trusted a plain GUC that any role can rewrite. tests/rls-spoof.integration.test.ts reproduces the leak on Postgres 17 with the old policy, then shows it is blocked.

    • DB layer (the real fix): Postgres cannot make a custom GUC read-only, so a SECURITY DEFINER setter alone would not help. Instead PgGuard also sets app.user_id_mac = HMAC-SHA256(key, 'pgguard:v1:'||user_id) and the policy calls pgguard.current_user_id(), a SECURITY DEFINER function that reads the key from a table the agent roles cannot read and returns the id only if the MAC verifies (docker/postgres/optional/rls-principal-verify.sql). A rewritten id gives NULL → no rows. I chose this over per-tenant roles because it needs one key, no role per user and no SET ROLE from the gateway.

    • Gateway layer: function calls are checked by walking the parsed SQL (AST) plus a quote-aware lexer, not by regex. set_config, dblink*, *fdw* functions, pg_terminate_backend/pg_cancel_backend, advisory locks, pg_sleep* and file/large-object functions are blocked, including "dblink"(, pg_catalog."SET_CONFIG"(, calls in CTEs/subqueries/WHERE, inside EXPLAIN, and SQL the parser cannot read. CREATE SERVER/FOREIGN TABLE/EXTENSION, USER MAPPING and U& escapes are refused.

  • Verified identity, fail closed. require_jwt: true by default; tokens must verify as HS256 with PGGUARD_JWT_SECRET. With no secret, tokens are refused instead of decoded. PGGUARD_DEV_INSECURE_JWT=1 restores decode-only/no-token behaviour for local dev only.

  • Row cap the agent can't raise. run_select uses min(max_rows, policy max_rows) and truncates returned rows, so a LIMIT inside a subquery no longer skips the cap.

  • TLS. sslmode=verify-ca now validates the certificate chain (it used to set rejectUnauthorized:false); verify-full also checks the hostname; unknown modes are refused.

Phase 2 — DB-native baseline:

  • docker/postgres/init/04-agent-roles.sql + scripts/provision-roles.sql: app_reader gets default_transaction_read_only=on, role-level statement_timeout / lock_timeout / idle_in_transaction_session_timeout, search_path=corp, NOBYPASSRLS, connection limits; writer/migrator get timeouts without read-only; REVOKE CREATE, USAGE ON SCHEMA public FROM PUBLIC; FORCE ROW LEVEL SECURITY on corp tables.

  • src/db/preflight.ts: on MCP startup, refuse to start if the connected role is superuser/BYPASSRLS, statement_timeout is 0, read-only default does not match the tier, or dblink/postgres_fdw is installed. whoami exposes the result under db_preflight.

  • DDL path (withClient) applies policy timeouts (previously unset).

  • Live PG17 tests: writes via WITH … DELETE / nextval fail with the classifier bypassed; role timeout cancels pg_sleep with no app SET LOCAL; preflight rejects a superuser.

Phase 3: allowed schemas and row/byte result caps (this PR):

  • allowed_schemas (defaults + per role, corp in the shipped policy) and an optional search_path (defaults to the allowlist; must be a subset of it). src/policy/schemaGuard.ts parses the SQL and refuses, before any DB call:

    • schema-qualified relations outside the list (FROM, JOIN, subqueries, CTE bodies, UNION branches, LATERAL, INSERT/UPDATE/DELETE targets, DDL names);

    • unqualified pg_* relations. Postgres searches pg_catalog first, and every pg_catalog relation is named pg_*;

    • qualified functions, types and column refs in other schemas (built-in pg_catalog functions are fine);

    • built-ins that run SQL from a string (query_to_xml, table_to_xml, ts_stat, crosstab, …);

    • SQL the parser cannot read (fail closed). It runs on run_select, explain_query, run_dml and run_ddl. list_schemas / list_tables are filtered, and list_tables / describe_table refuse other schemas. PgGuard also sets search_path per transaction, so unqualified names resolve the way the guard assumes.

  • Result caps. run_select no longer appends LIMIT to the SQL text. It runs DECLARE pgguard_result NO SCROLL CURSOR FOR <sql> inside the READ ONLY transaction and FETCHes batches of 50 until it has max_rows + 1 rows, so the extra row shows the result was cut. It also stops at max_result_bytes, measured as compact JSON (default 1 MiB, reader 512 KiB). Cells over max_cell_bytes (default 64 KiB) are cut: text on a UTF-8 boundary, bytea returned as \x… hex, json as cut text. The response reports truncated, truncated_by (rows / bytes / cells), truncated_cells, truncated_columns and result_bytes. max_rows is still min(argument, policy). explain_query output gets the same byte budget.

  • whoami shows allowed_schemas, search_path, max_result_bytes and max_cell_bytes.

  • Live PG17 tests:

    • a schema with no grants is refused by Postgres (42501) with the guard turned off;

    • a schema that is granted but not allowlisted is refused by the guard and readable with the guard off, so the two layers are independent;

    • 10k-row and wide-row tables are cut by rows and by bytes;

    • 200 KB text and bytea cells are cut to 64 KiB;

    • neither max_rows: 1e9 nor a LIMIT in a subquery raises the cap.

Still true / not fixed yet (phases 4 and 5 are described above):

  • The schema guard is a UX layer and early deny, not the boundary. DB grants are the boundary. Gaps the guard does not cover:

    • string-to-OID casts ('hr.t'::regclass, to_regclass) and size/metadata functions can show that an object exists;

    • temp tables (pg_temp) are searched before the path;

    • any SQL the parser cannot read is refused, which is stricter than Postgres. Some valid queries (TABLE t, SELECT … FROM ONLY t, DELETE … USING, some DDL) now need rewriting for roles with an allowlist. Leave allowed_schemas unset to turn the guard off.

  • Byte caps apply to what the agent receives, not to what crosses the wire. Each cell is fetched in full before it is cut. That is bounded by statement_timeout and Postgres's 1 GB field limit. Plans with Sort/Hash/aggregates still compute their full input before the first FETCH.

  • Schema allowlists in YAML and GRANTs in the DB can drift. Nothing generates one from the other yet.

  • The token is still a tool argument. It must be signed, but an agent holding several users' tokens can pick among them.

  • The HMAC key is static per deployment, with no expiry or session binding; an agent can read its own MAC. Key rotation is manual (pgguard.set_rls_key). Avoid log_statement=all / parameter logging, which would record MACs.

  • Function blocking is a denylist; a new or extension function with side effects is not caught. A non-superuser can still SET default_transaction_read_only TO off — grants and BEGIN READ ONLY remain the write hard-stop for readers. confirm:true alone is agent-supplied; phase 4 adds signed out-of-band approvals on top (see above). Phase 5 adds a DB host allowlist, a hash-chained audit log and CI against Postgres 17 (see above).

  • Old RLS policies that read current_setting('app.user_id') directly are still spoofable at the DB layer; move them to pgguard.current_user_id().

Also: use least-privilege DB roles, keep DML/DDL behind confirm, ship data/audit/ off the box, and treat Compose Vault as dev-only.

Full guidance: SECURITY.md · GOVERNANCE.md · ARCHITECTURE.md


Contributing / support

Contributions are welcome — new here? Start with a good first issue or help wanted.

Doc

Purpose

CONTRIBUTING.md

Setup (Node 20), tests incl. live Postgres 17, branch/PR flow, coding standards

CODE_OF_CONDUCT.md

Contributor Covenant 2.1

SECURITY.md

Threat model; report vulnerabilities via private advisories

Issue templates / PR template

What to include

Quick start for contributors: npm ci && npm test && npm run build. Please read the docs table above before changing policy or MCP tools.

Available Tools

10 tools
db_healthA

Ping Postgres and return server version

NameRequiredDescriptionDefault

No parameters

TDQS

A4.1/5.0
Behavior3/5

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

With no annotations provided, the description carries the burden of behavioral disclosure. It conveys that the operation is a non-mutating connectivity check and names the output (server version), but does not describe failure behavior, authentication needs, or whether any state changes could occur.

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?

The description is eight words: short, front-loaded with the action, and every word adds meaning. There is no redundancy or filler.

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?

For a zero-parameter health-check tool with no output schema, the description states the operation and the returned value, which covers all the essential context an agent needs. Despite its brevity, it is complete for the tool's simplicity.

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?

The input schema has zero parameters, so the baseline is 4. There are no parameter semantics to explain, and the description correctly adds no irrelevant parameter information.

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 uses a specific verb ('Ping') and resource ('Postgres') and states that it returns the server version. This clearly distinguishes it from sibling tools like list_schemas, run_select, or run_dml, which all perform different database operations.

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 usage context is implied: call this tool to verify database connectivity and retrieve the Postgres version. However, there is no explicit when-to-use versus alternative guidance or mention of exclusions, though the zero-parameter, health-check nature makes this less critical.

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

describe_tableC

Describe columns for a table

NameRequiredDescriptionDefault
tableYes
schemaYes

TDQS

C2.7/5.0
Behavior2/5

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

With no annotations, the description carries the full burden of behavioral disclosure. While 'Describe' semantically implies a read-only operation, the description does not state what is returned, whether the table must exist, or how errors are handled.

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

Conciseness4/5

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

The description is a single focused sentence with no redundant information. It is concise and front-loaded, though slightly under-specified in content.

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

Completeness2/5

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

There is no output schema, no annotations, and no usage guidance. For a two-parameter introspection tool, this minimal description leaves gaps around expected return values and when to choose this tool over siblings.

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

Parameters2/5

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

Schema description coverage is 0%, and the description only mentions 'table,' not the 'schema' parameter or the relationship between them. The parameter names are somewhat self-explanatory, but the description does not compensate for the lack of schema-level documentation.

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

Purpose4/5

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

The description uses a clear verb and resource: 'Describe columns for a table.' This sufficiently identifies the tool's purpose and distinguishes it from siblings like list_tables or run_select, though it does not explicitly differentiate itself from alternatives.

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

Usage Guidelines2/5

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

There is no guidance on when to use this tool versus siblings such as list_tables, explain_query, or run_select. No context is provided for the intended workflow, such as inspecting schema before writing queries.

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

explain_queryC

EXPLAIN a query; ANALYZE optional and gated by policy

NameRequiredDescriptionDefault
sqlYes
analyzeNo

TDQS

C2.8/5.0
Behavior2/5

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

No annotations are provided, so the description bears the full burden of disclosing behavior. It reveals that ANALYZE is policy-gated but does not disclose that ANALYZE typically executes the query and can have side effects, nor does it state whether plain EXPLAIN is read-only.

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?

The description is a compact two-clause sentence with no filler, and the core operation is front-loaded before the optional variant. Every part carries meaning.

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

Completeness2/5

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

For a tool with no output schema and no annotations, the description leaves key operational context unstated: whether ANALYZE executes the query, what policy failure looks like, and what the call returns. The two-parameter surface is simple, but the missing behavior and return-value context make it incomplete.

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

Parameters2/5

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

Schema description coverage is 0%, so the description must compensate, but it only loosely maps 'query' to sql and 'ANALYZE optional' to analyze. It does not clarify what kinds of SQL are accepted, what the boolean controls beyond the SQL keyword, or what output the agent should expect.

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

Purpose4/5

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

The description names a specific operation, EXPLAIN, and a resource, the supplied query, which distinguishes it from sibling tools like run_select and run_dml. It stops short of a 5 because it never explicitly says this returns an execution plan rather than executing the query.

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

Usage Guidelines2/5

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

There is no guidance about when to choose explain_query over run_select, run_dml, or run_ddl, nor any statement about the intended scenario such as inspecting a plan before execution. The only usage hint is that ANALYZE is optional and policy-gated, which is a constraint rather than a use-case guide.

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

get_audit_tailA

Return last N append-only audit events

NameRequiredDescriptionDefault
nNoNumber of events (default 20)

TDQS

A3.8/5.0
Behavior3/5

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

No annotations are provided, so the description carries the behavioral burden. 'Return' signals a read-only operation and 'append-only' adds useful context about event immutability, but there is no disclosure of ordering guarantees, pagination, or potential operational caveats.

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?

A single sentence with no redundant words. The core behavior is front-loaded and the description earns its place without padding.

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 read tool with one optional parameter and no nested schema, the description is largely sufficient. It clearly indicates what is returned ('audit events') and the count semantics, though the lack of an output schema means the exact event structure is left unspecified.

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%, and the parameter 'n' is already documented as 'Number of events (default 20)'. The description's 'last N' ties the parameter to ordering, but adds little beyond what the schema already conveys.

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 states a specific verb ('Return'), a clear resource ('last N append-only audit events'), and precise scoping. It is easily distinguished from the sibling database and health tools, which deal with schemas, tables, queries, and system status.

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 use when recent audit events are needed, but it does not explicitly state when to prefer this tool over alternatives or provide any exclusion conditions. No sibling tool obviously overlaps, so the lack of explicit guidance is a minor gap.

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

list_schemasA

List non-system schemas

NameRequiredDescriptionDefault

No parameters

TDQS

A3.5/5.0
Behavior3/5

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

With no annotations, the description carries the full behavioral disclosure burden. It does add a meaningful behavioral trait by stating only non-system schemas are listed, implying a read-only operation, but it does not disclose permission requirements, what qualifies as a system schema, or any ordering/limitation behavior.

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?

A single, compact sentence with no filler. The key qualifier 'non-system' is front-loaded and every word earns its place.

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 zero-parameter, no-output-schema listing tool, the description is nearly complete: it names the resource, the scope, and the filter. It only lacks explicit context about sibling-tool routing, but this is a minor gap given the simplicity.

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?

The tool has zero parameters, so the baseline is 4. The only relevant context, the 'non-system' filter, is already stated in the description rather than left implicit.

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

Purpose4/5

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

The description states a specific verb ('List') and resource ('non-system schemas'), clearly conveying what the tool does. It does not explicitly contrast with sibling tools like list_tables, so it stops short of full differentiation.

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

Usage Guidelines2/5

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

No guidance is provided on when to use this tool versus list_tables, describe_table, or reporting tools like db_health. The phrase 'non-system schemas' implies a filtering use case, but exclusions and alternatives are left unstated.

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

list_tablesA

List tables, optionally filtered by schema

NameRequiredDescriptionDefault
schemaNoSchema name (optional)

TDQS

A3.8/5.0
Behavior3/5

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

No annotations are provided, so the description carries the full burden of behavioral disclosure. It conveys a read-oriented enumeration operation with optional filtering, but does not mention permissions, inclusion of views, or output behavior. The core behavior is clear but not deeply detailed.

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?

A single concise sentence with no filler. The primary operation is front-loaded, and the optional filter is stated right after.

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 one-optional-parameter listing tool, the description and schema are sufficient for an agent to invoke it correctly. There is no output schema or annotations, but the missing behavioral details are not critical for basic correct 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 description coverage is 100%, and the single 'schema' parameter is already documented in the input schema as optional. The description adds no new semantic detail beyond restating the optional filter.

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 directly states a specific action (list) on a specific resource (tables), with an optional schema filter. This clearly differentiates it from siblings like list_schemas and describe_table.

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 optional schema filtering implies the main use case: enumerating tables, optionally scoped to a schema. However, there is no explicit guidance about when not to use this tool or which sibling tool to choose instead.

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

run_ddlA

CREATE/ALTER/DROP for migrator/admin; requires confirm:true; optional dry_run

NameRequiredDescriptionDefault
sqlYes
confirmNo
dry_runNo

TDQS

A4.1/5.0
Behavior4/5

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

With no annotations, the description carries the safety burden. It discloses the destructive nature via DROP, the required confirm:true gate, and dry_run availability, giving essential safety cues 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?

A single dense sentence front-loads the operation class, role restriction, and safety requirements. No filler words.

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

Completeness3/5

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

Adequate for selecting the tool, but gaps remain for a destructive tool with no annotations and no output schema: dry_run semantics, confirm=false behavior, and postconditions are not described.

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 0%, so the description must compensate. It adds meaning by clarifying sql contains DDL and confirm must be true, but does not explain dry_run's effect or what happens if confirm is omitted/false.

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?

States specific DDL operations (CREATE/ALTER/DROP) and the target audience (migrator/admin), clearly differentiating it from siblings like run_select, run_dml, and explain_query.

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 clear context: this is for DDL operations and requires confirm:true, making the intended use explicit. It doesn't explicitly name alternatives or when-not-to-use, but the DDL scope and sibling names make the boundary obvious.

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

run_dmlA

INSERT/UPDATE/DELETE when role allows; requires confirm:true

NameRequiredDescriptionDefault
sqlYes
confirmYes

TDQS

A3.8/5.0
Behavior3/5

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

With no annotations, the description assumes full transparency burden and does reveal two important behavioral traits: authorization gating and a mandatory confirmation flag. However, it does not disclose what the tool returns, how it handles errors, whether multiple statements are permitted, or the irreversible/mutating nature of DML beyond the operation names themselves.

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?

The description is a single, tightly written sentence with zero filler. It front-loads the core operation scope and then includes the two most important invocation constraints, making it highly efficient for an agent to parse.

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

Completeness3/5

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

Given the tool has only two required parameters and no output schema, the description covers the essential invocation details: operation type, authorization limit, and mandatory confirmation. Still, for a potentially destructive DML tool, it leaves gaps around result behavior, error handling, and execution semantics, so it is adequate but not fully complete.

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

Parameters2/5

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

Schema coverage is 0%, so the description must compensate for parameter documentation. It usefully explains that confirm must be true, but it provides no additional semantics for sql beyond implying it contains an INSERT, UPDATE, or DELETE statement. The sql parameter still lacks format guidance, constraints, or examples.

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 explicitly states the tool executes INSERT/UPDATE/DELETE statements, giving a specific verb, resource category, and operation scope. This clearly distinguishes it from sibling tools like run_select and run_ddl, and the role/confirm caveat adds functional specificity.

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 clearly indicates that this tool is for DML operations, which is an unambiguous use case distinct from the query, DDL, and diagnostic siblings. It also provides a clear precondition ('when role allows') and a mandatory invocation requirement ('requires confirm:true'), though it does not explicitly name alternatives or when-not conditions.

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

run_selectA

Run a single SELECT (max rows enforced; read-only role preferred)

NameRequiredDescriptionDefault
sqlYes
max_rowsNo

TDQS

A3.9/5.0
Behavior3/5

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

No annotations are provided, so the description carries the disclosure burden. It does reveal two behaviors: max rows are enforced and read-only role is preferred. However, it does not explain what happens when the limit is reached, whether queries can time out, or what the response shape is.

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?

A single sentence that front-loads the action and packs essential constraints into a parenthetical. No wasted words or redundant restatement of the schema.

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

Completeness3/5

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

The tool is relatively simple and the description covers the core execution contract, but there is no output schema and no mention of return format, error behavior, or when a sibling tool would be a better choice. The description is adequate but not fully complete.

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 0%, so the description must add meaning to the parameters. It adds meaning to max_rows ('enforced') and implies sql should contain a SELECT statement. It does not clarify optionality, defaults, or SQL formatting, so compensation is only partial.

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 names a precise verb and resource: execute a single SELECT statement. It also distinguishes itself from siblings like run_dml and run_ddl by limiting scope to SELECT queries, and 'max rows enforced' adds a meaningful boundary.

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 gives clear usage context: run one SELECT query, with a row limit and a read-only role preference. It does not explicitly name alternatives or state when not to use it, but the SELECT designation and sibling names make the primary use case evident.

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

whoamiA

Current agent role and effective policy

NameRequiredDescriptionDefault

No parameters

TDQS

A3.6/5.0
Behavior3/5

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

With no annotations, the description carries the burden of explaining behavior. It indicates this is an introspection returning the current role and policy, but it does not explicitly state that the operation is read-only, what the response format is, or whether any policy evaluation side effects occur. This is adequate but not rich.

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?

The description is a single seven-word phrase with no filler or redundancy. The core identity/policy scope is front-loaded and every word earns its place.

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

Completeness3/5

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

The tool has no parameters and a minimal schema, but no output schema exists, so the description should clarify the return shape. It names the two pieces of information (role and effective policy) but not their format, types, or nesting, leaving an agent to guess how to parse the response.

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?

The tool has no parameters and the schema coverage is 100%, so there is no parameter burden for the description to carry. The baseline of 4 applies because there are zero parameters to document.

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

Purpose4/5

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

The phrase 'Current agent role and effective policy' clearly identifies the resource as the agent's identity and policy context. It is distinct from the database-focused siblings (db_health, list_tables, run_select, etc.), though it lacks an explicit verb such as 'returns' or 'displays'.

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?

Usage is implied: an agent should call this when it needs to know the current role or effective policy. It gives no explicit when-to-use or when-not-to-use guidance and does not mention alternatives, but the tool is unique among the siblings, so the omission is less harmful.

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.

  1. 10 tool updatesv1.0.0
    • First observeddb_health
    • First observeddescribe_table
    • First observedexplain_query
    • First observedget_audit_tail
    • First observedlist_schemas
    • First observedlist_tables
    • First observedrun_ddl
    • First observedrun_dml
    • First observedrun_select
    • First observedwhoami

TDQS

A3.6/5.0

Scored across 10 tools

Disambiguation5/5

Each tool maps to a distinct operation: health check, schema inspection, row queries, DML, DDL, explain, audit, and role/policy lookup. Even though run_select and explain_query both accept queries, their purposes are clearly separated.

Naming Consistency4/5

Most tools follow a clear verb_noun snake_case convention: list_schemas, list_tables, describe_table, run_select, explain_query, get_audit_tail. Minor deviations are db_health and whoami, which do not follow the verb_noun pattern but remain understandable.

Tool Count5/5

Ten tools is well-scoped for a Postgres guard agent. Each tool serves a necessary purpose in the lifecycle of inspecting and safely modifying a database.

Completeness4/5

The tool surface covers health, schema discovery, SELECT/DML/DDL execution, query planning, auditing, and policy introspection. Minor gaps like explicit transaction control or listing roles/indexes are not critical, but could make some workflows more seamless.

Maintenance

ActivityNo data
ResponsivenessSyncing

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Enables secure querying of PostgreSQL databases through MCP-compatible clients. Supports read-only SQL execution, table exploration, and connection management with built-in security validation.
    3
    64 npm
    9
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables querying and modifying PostgreSQL databases through MCP tools with read/write operations, schema inspection, and write-safety constraints that limit modifications to the mcp schema.
    1
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables querying PostgreSQL databases via MCP, with multi-database routing, credential isolation, and truncated results plus full CSV export.
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables connecting Postgres databases to MCP clients with built-in query and inspection tools, custom parametrized SQL tools, scoped API keys, group permissions, and audit logging.
    6
    MIT