PgGuard Agent
PgGuard Agent provides governed PostgreSQL access for MCP clients: health checks, schema discovery, capped reads, gated writes/DDL, explain, audit, and role introspection.
Ping the database and return the PostgreSQL server version.
List non-system schemas and tables, optionally filtered by schema.
Describe a table’s columns by schema and table name.
Run single SELECT queries with
max_rowsand policy-enforced read limits.Run INSERT/UPDATE/DELETE only with
confirm:true, subject to role and policy enforcement.Run CREATE/ALTER/DROP with
confirm:trueand optionaldry_run, for migrator/admin roles.EXPLAIN queries, with ANALYZE only when policy allows.
Read the latest append-only audit events, with a configurable count.
Show the current effective agent role and policy.
Provides governed access to PostgreSQL databases, including role-based access control, schema and table introspection, row-capped SELECT queries, confirm-gated DML and DDL operations, EXPLAIN support, and append-only audit logging.
Optionally retrieves PostgreSQL credentials and connection settings from HashiCorp Vault KV v2 instead of environment variables.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@PgGuard AgentExplain the query that joins orders and customers"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 |
Components, data flow, MCP surface | |
Threat model, secrets, confirm gates | |
Roles, policy.yaml, audit expectations | |
Rancher Desktop / Compose local walkthrough |
Related MCP server: Postgres MCP Server
What you get
MCP tools
Tool | Purpose |
| Ping + Postgres version |
| Safe introspection, limited to the role's |
|
|
| Data changes — requires |
| Schema changes — requires |
| Queue one exact DML/DDL statement for a human approver (returns |
|
|
| Recent hash-chained audit events (not granted to |
| 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) |
| Analyst / read-only agent |
|
| App DML with confirmation |
|
| Schema changes with confirmation |
|
| 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 build2. Create least-privilege DB users (recommended)
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.sqlCompose 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 certificateAdd 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
Replace
REPLACE_WITH_ABS_PATHwith the absolute path to this repo.Set
PG*(or Vault) env to your database — not the democorpdbvalues.Paste into Cursor MCP settings and reload MCP servers.
Or run stdio directly:
npm run mcp5. Smoke-test tools
whoami→reader(or your chosen role)db_health→ version stringlist_tablesfor your schemarun_select→ limited rowsrun_dmlasreader→ denied; aswriterwithconfirm: true→ allowedget_audit_tail(writer and up; reader no longer hasaudit_read) → events underdata/audit/;npm run audit:verifychecks 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 mcpSeeded 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 |
| Compile TypeScript → |
| Start stdio MCP server |
| Unit tests (+ live Postgres tests when |
| Validate |
| Approver CLI: |
| 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
ROLLBACKThe classifier is the first gate. Postgres is the second: a
SELECTthat calls a writing function, orEXPLAIN ANALYZEof DML, fails withcannot execute … in a read-only transaction.Timeouts come from
defaultsinconfig/policy.yaml(statement_timeout_ms,lock_timeout_ms,idle_in_transaction_session_timeout_ms; per-role overrides;0disables viaSET LOCAL). Role-levelALTER 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_ddlwith a principal run inBEGIN … COMMITfor 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 testWithout 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 -> COMMITdry_run: trueneeds noconfirm. It returnsrowCount,would_exceed_capand asamplefrom aRETURNINGclause. 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_rowscomes fromconfig/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
The agent calls
request_approvalwithtool(run_dml/run_ddl) and the exact SQL. PgGuard classifies it, runs the schema guard, does a rolled-back dry run for DML, and writesdata/approvals/pending/<approval_id>.json. It returnsapproval_idand the SQL's sha256.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.)The agent retries
run_dml/run_ddlwith the same SQL,confirm: trueandapproval_id(or the printedapproval_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_KEYorPGGUARD_APPROVAL_PUBLIC_KEY_FILE). It refuses to start if anyPGGUARD_APPROVAL_PRIVATE_KEY*variable is in its environment.Bound to the SQL. The token carries
sha256of 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 JWTsub(null when there is no principal). All of these come from the live call, never from the token.Expiring.
exp - iatmust 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 foriat.Single use. Each token has a random 128-bit
jti, recorded in a ledger:ledger: file(default):data/approvals/used/<jti>created withO_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.sqlinstallspgguard.consume_approval(), a SECURITY DEFINER function writing to a table agent roles cannot read or write. Forrun_dmlit runs inside the DML transaction, so the approval is spent only if the write commits. It also rejects a token whoseexphas 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 usesmin(policy max_affected_rows, approved max).No self-approval.
approvermust differ from the requestingsub.Fail closed. With
required_for: [dml, ddl](shipped default) and no public key configured, real runs are refused.dry_runnever needs an approval.whoamishowsapprovals.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)
approveris 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. Useledger: postgreswhen 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 thepgguardschema for roles limited tocorp.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) setrequiredFor: []because they test caps and RLS, not approvals.npm testnow runs Jest with--runInBand. Live suites runALTER ROLE(cluster-wide) in their setup, and in parallel one suite could reset another'sstatement_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:
checks the credential host against
allowed_db_hosts: exact names, a leading*.suffixwildcard (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;resolves the name once and refuses link-local and unspecified addresses (
169.254.0.0/16including cloud metadata,fe80::/10,0.0.0.0/8,::) whatever the policy says. Withallowed_db_cidrsset, every resolved address must be inside one of them;requires
sslmode=verify-fullunlessplaintext_loopback_ok: trueand every resolved address is loopback (the shipped local-demo setting; turn it off anywhere shared);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-fullconnects over TLSv1.3 with the right CA and name;the wrong name is rejected, and an unrelated CA bundle is rejected;
preflight
ssl_in_usefails 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) plushostssl-onlypg_hba.confrules for the agent roles from the PgGuard subnet, and nodblink/postgres_fdw(preflight already refuses them).allowed_db_cidrsis 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:verifywould 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 NULLlet 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.tsreproduces 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 callspgguard.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 noSET ROLEfrom 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, insideEXPLAIN, and SQL the parser cannot read.CREATE SERVER/FOREIGN TABLE/EXTENSION,USER MAPPINGandU&escapes are refused.
Verified identity, fail closed.
require_jwt: trueby default; tokens must verify as HS256 withPGGUARD_JWT_SECRET. With no secret, tokens are refused instead of decoded.PGGUARD_DEV_INSECURE_JWT=1restores decode-only/no-token behaviour for local dev only.Row cap the agent can't raise.
run_selectusesmin(max_rows, policy max_rows)and truncates returned rows, so aLIMITinside a subquery no longer skips the cap.TLS.
sslmode=verify-canow validates the certificate chain (it used to setrejectUnauthorized:false);verify-fullalso checks the hostname; unknown modes are refused.
Phase 2 — DB-native baseline:
docker/postgres/init/04-agent-roles.sql+scripts/provision-roles.sql:app_readergetsdefault_transaction_read_only=on, role-levelstatement_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 SECURITYoncorptables.src/db/preflight.ts: on MCP startup, refuse to start if the connected role is superuser/BYPASSRLS,statement_timeoutis 0, read-only default does not match the tier, ordblink/postgres_fdwis installed.whoamiexposes the result underdb_preflight.DDL path (
withClient) applies policy timeouts (previously unset).Live PG17 tests: writes via
WITH … DELETE/nextvalfail with the classifier bypassed; role timeout cancelspg_sleepwith no appSET LOCAL; preflight rejects a superuser.
Phase 3: allowed schemas and row/byte result caps (this PR):
allowed_schemas(defaults + per role,corpin the shipped policy) and an optionalsearch_path(defaults to the allowlist; must be a subset of it).src/policy/schemaGuard.tsparses 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 searchespg_catalogfirst, and everypg_catalogrelation is namedpg_*;qualified functions, types and column refs in other schemas (built-in
pg_catalogfunctions 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_dmlandrun_ddl.list_schemas/list_tablesare filtered, andlist_tables/describe_tablerefuse other schemas. PgGuard also setssearch_pathper transaction, so unqualified names resolve the way the guard assumes.
Result caps.
run_selectno longer appendsLIMITto the SQL text. It runsDECLARE pgguard_result NO SCROLL CURSOR FOR <sql>inside the READ ONLY transaction andFETCHes batches of 50 until it hasmax_rows + 1rows, so the extra row shows the result was cut. It also stops atmax_result_bytes, measured as compact JSON (default 1 MiB, reader 512 KiB). Cells overmax_cell_bytes(default 64 KiB) are cut: text on a UTF-8 boundary,byteareturned as\x…hex,jsonas cut text. The response reportstruncated,truncated_by(rows/bytes/cells),truncated_cells,truncated_columnsandresult_bytes.max_rowsis stillmin(argument, policy).explain_queryoutput gets the same byte budget.whoamishowsallowed_schemas,search_path,max_result_bytesandmax_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: 1e9nor aLIMITin 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. Leaveallowed_schemasunset 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_timeoutand Postgres's 1 GB field limit. Plans with Sort/Hash/aggregates still compute their full input before the firstFETCH.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). Avoidlog_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 andBEGIN READ ONLYremain the write hard-stop for readers.confirm:truealone 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 topgguard.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 |
Setup (Node 20), tests incl. live Postgres 17, branch/PR flow, coding standards | |
Contributor Covenant 2.1 | |
Threat model; report vulnerabilities via private advisories | |
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 toolsdb_healthA
Ping Postgres and return server version
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
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.
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.
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.
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.
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.
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
| Name | Required | Description | Default |
|---|---|---|---|
| table | Yes | ||
| schema | Yes |
TDQS
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.
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.
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.
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.
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.
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
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | ||
| analyze | No |
TDQS
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.
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.
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.
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.
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.
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
| Name | Required | Description | Default |
|---|---|---|---|
| n | No | Number of events (default 20) |
TDQS
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.
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.
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.
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.
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.
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
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
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.
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.
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.
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.
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.
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
| Name | Required | Description | Default |
|---|---|---|---|
| schema | No | Schema name (optional) |
TDQS
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.
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.
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.
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.
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.
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
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | ||
| confirm | No | ||
| dry_run | No |
TDQS
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.
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.
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.
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.
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.
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
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | ||
| confirm | Yes |
TDQS
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.
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.
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.
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.
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.
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)
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | ||
| max_rows | No |
TDQS
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.
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.
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.
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.
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.
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
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
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.
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.
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.
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.
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.
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.
10 tool updates
v1.0.0- First observed
db_health - First observed
describe_table - First observed
explain_query - First observed
get_audit_tail - First observed
list_schemas - First observed
list_tables - First observed
run_ddl - First observed
run_dml - First observed
run_select - First observed
whoami
TDQS
Scored across 10 tools
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.
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.
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.
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
Related MCP Connectors
Managed Postgres MCP: projects, SQL, docs search, and storage buckets via OAuth.
Guard AI agents' PostgreSQL/MySQL access via MCP: SQL audit, auth, masking, write approval
Hosted MCP server for PostgreSQL diagnostics: slow queries, missing indexes, connection pressure.
Governed MCP gateway: one endpoint for your tools, with credential custody and audit log.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables secure querying of PostgreSQL databases through MCP-compatible clients. Supports read-only SQL execution, table exploration, and connection management with built-in security validation.364 npm9MIT
- FlicenseNot gradedqualityCmaintenanceEnables 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-
- FlicenseNot gradedqualityDmaintenanceEnables querying PostgreSQL databases via MCP, with multi-database routing, credential isolation, and truncated results plus full CSV export.-
- AlicenseNot gradedqualityCmaintenanceEnables 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.6MIT