Skip to main content
Glama
adaptive-pg-mcp (PostgreSQL)

adaptive-pg-mcp (PostgreSQL)

adaptive-pg-mcp

An MCP server for PostgreSQL. Agents get typed tools on named databases, an organization's reviewed SQL as named tools with typed parameters, and free SQL that runs under the grants of the login each call uses. It is one static Go binary that serves Streamable HTTP and stdio to any MCP client. Behind the Straza gateway, each agent gets only the tools and databases its policy allows, with approval holds and an audit record per call, and the server needs no change for it.

Postgres 14 to 18 are supported. The server targets MCP revision 2026-07-28 and also serves the earlier revisions that the official Go SDK serves.

From release 0.1.0 on, a change to server.yaml, access.yaml or tools.yaml only adds to the format, and CHANGELOG.md records each one. The design is docs/DESIGN.md.

What the server enforces

On every call:

  • A call names one database from server.yaml and runs on a login of that database.

  • Each statement goes to Postgres as one unnamed prepared statement on the extended protocol, and each call runs in one transaction, with statement_timeout set from the configured caps. A text that holds two statements is refused.

  • Read tools and query_sql run in a READ ONLY transaction. A read stops at the row cap or the byte cap and returns truncated: true when more rows follow.

  • A write that changes more rows than write_rows is rolled back. The count includes rows changed by triggers, cascades, functions and WITH queries. With track_counts off, every write is refused, because the count cannot be read.

  • execute_sql and execute_ddl tell row changes from structure changes by what Postgres reports: the command tag and the rows changed in the system catalogs. The server never parses the SQL text, so a CREATE TABLE inside a DO block is rolled back in execute_sql like a plain one.

  • Free SQL and catalog tools close their connection after each call, so session state such as a SET or an advisory lock never reaches the next call.

  • The table tools quote every identifier and send every value as a bind parameter.

  • A cancelled call cancels its statement in Postgres.

On every request:

  • Over HTTP, the server compares the SHA-256 of each request's bearer token with the configured hashes, in constant time. A protocol session id is never taken as proof of who calls.

  • A request with an Origin header is refused, because the server serves no web page. A request body over 1 MiB is refused.

  • In standalone mode, access.yaml decides which tools, databases and tables each caller may use, and tools/list shows each caller only its own tools.

  • Without tls, HTTP listens on a loopback address only, unless allow_plain_http: true says that something in front encrypts the traffic.

  • No token or connection string appears in a log line or an error. The caller's token is never sent to Postgres or anywhere else.

Before it serves, the start checks refuse a login that is a superuser while free SQL is on, a database with dblink or postgres_fdw installed, logins of one database that reach different databases, and a catalog statement that does not prepare or whose login lacks a grant it needs.

What it leaves to Postgres:

  • The grants of each login decide what it may read and change. Free SQL runs with every grant the login holds on its database. A READ ONLY transaction stops writes to tables, but a function the login may execute still runs. For a hard boundary, give reads a login with SELECT only, and give the write login only the grants its writes need.

  • Row-level security and column privileges apply to every call, because every statement runs as the login. The server does not filter rows per person.

Related MCP server: postgres-mcp

Quick start

The compose example starts Postgres 18 with a helpdesk database, a read login and a write login, and the server in front of it, built from this repository.

git clone https://github.com/strazahq/adaptive-pg-mcp
cd adaptive-pg-mcp/examples/compose
docker compose up --build

The server listens on http://127.0.0.1:8080/mcp and accepts the bearer token demo-token. Add it to Claude Code:

claude mcp add --transport http helpdesk-demo http://127.0.0.1:8080/mcp \
  --header "Authorization: Bearer demo-token"

Or check that it responds:

token=demo-token
curl -s http://127.0.0.1:8080/mcp \
  -H "Authorization: Bearer $token" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'

The demo serves every tool, with free_sql: read-write, and the two catalog tools of examples/catalog. It serves plain HTTP inside the compose network and publishes the port on 127.0.0.1 only. The passwords and the token are for the demo only. docker compose down -v removes the containers and the data.

Install

Container image

docker pull ghcr.io/strazahq/adaptive-pg-mcp:0.1.1

The image holds the static binary on distroless static and runs as the nonroot user, uid 65532. Its default command is serve -config /etc/adaptive-pg-mcp/server.yaml. It is built for linux/amd64 and linux/arm64. A release tag vX.Y.Z publishes X.Y.Z, and each push to main publishes main and sha-<commit>. Each image carries a signed build provenance attestation, and from 0.1.1 on an SPDX SBOM that scanners such as Trivy, Grype and Docker Scout read from the registry:

gh attestation verify oci://ghcr.io/strazahq/adaptive-pg-mcp:0.1.1 --owner strazahq
docker buildx imagetools inspect ghcr.io/strazahq/adaptive-pg-mcp:0.1.1 --format '{{ json .SBOM }}'

Release binaries

Each release holds a static binary for Linux, macOS and Windows on amd64 and arm64, with a SHA256SUMS file and a signed build provenance attestation:

curl -fsSLO https://github.com/strazahq/adaptive-pg-mcp/releases/download/v0.1.1/adaptive-pg-mcp_0.1.1_linux_amd64.tar.gz
curl -fsSLO https://github.com/strazahq/adaptive-pg-mcp/releases/download/v0.1.1/SHA256SUMS
sha256sum --check --ignore-missing SHA256SUMS
gh attestation verify adaptive-pg-mcp_0.1.1_linux_amd64.tar.gz --owner strazahq
tar -xzf adaptive-pg-mcp_0.1.1_linux_amd64.tar.gz

From 0.1.1 on, each archive has an SPDX SBOM of its binary beside it, such as adaptive-pg-mcp_0.1.1_linux_amd64.spdx.json. SHA256SUMS lists it, and a signed attestation binds it to its archive:

gh attestation verify adaptive-pg-mcp_0.1.1_linux_amd64.tar.gz --owner strazahq \
  --predicate-type https://spdx.dev/Document/v2.3

The Windows archives are .zip files. macOS marks a downloaded binary as quarantined, and xattr -d com.apple.quarantine adaptive-pg-mcp lifts that.

go install

GOTOOLCHAIN=go1.27.3 go install github.com/strazahq/adaptive-pg-mcp/cmd/adaptive-pg-mcp@latest

go install builds with the Go you run and ignores the toolchain that go.mod pins. Earlier releases, such as Go 1.26.5, carry standard library vulnerabilities that govulncheck finds on the server's code paths, so set GOTOOLCHAIN as above, or run Go 1.27.3 or newer.

From source

git clone https://github.com/strazahq/adaptive-pg-mcp
cd adaptive-pg-mcp
CGO_ENABLED=0 go build -trimpath -o adaptive-pg-mcp ./cmd/adaptive-pg-mcp

Inside the module, the go command fetches the toolchain that go.mod pins when the installed Go is older. docker build -t adaptive-pg-mcp . builds the image.

Connect a client

Over Streamable HTTP, the endpoint is /mcp, and each request sends Authorization: Bearer <token>. Over stdio, the client starts adaptive-pg-mcp serve itself, with a server.yaml whose listen is stdio, the token in ADAPTIVE_PG_MCP_TOKEN and each connection string in the variable that server.yaml names. serve checks the token once at start and does not start without a match.

A server.yaml for stdio:

version: 1
mode: standalone
listen: stdio
free_sql: read
limits: { rows: 1000, bytes: 1048576, timeout: 30s, write_rows: 100 }
access: ./access.yaml
catalog: none
databases:
  helpdesk:
    logins:
      read:  { dsn_env: HELPDESK_READ_DSN }
      write: { dsn_env: HELPDESK_WRITE_DSN }

The examples below use https://mcp.example.internal:8443/mcp for HTTP and /etc/adaptive-pg-mcp/stdio.yaml for stdio. The client machine must trust the CA that signed the server's certificate.

HTTP:

claude mcp add --transport http adaptive-pg-mcp https://mcp.example.internal:8443/mcp \
  --header "Authorization: Bearer $ADAPTIVE_PG_MCP_TOKEN"

stdio:

claude mcp add --env ADAPTIVE_PG_MCP_TOKEN="$ADAPTIVE_PG_MCP_TOKEN" \
  --env HELPDESK_READ_DSN="$HELPDESK_READ_DSN" \
  --env HELPDESK_WRITE_DSN="$HELPDESK_WRITE_DSN" \
  --transport stdio adaptive-pg-mcp \
  -- adaptive-pg-mcp serve -config /etc/adaptive-pg-mcp/stdio.yaml

Both commands write the values into the Claude Code config. A project .mcp.json can read them from the environment instead:

{
  "mcpServers": {
    "adaptive-pg-mcp": {
      "type": "http",
      "url": "https://mcp.example.internal:8443/mcp",
      "headers": {
        "Authorization": "Bearer ${ADAPTIVE_PG_MCP_TOKEN}"
      }
    }
  }
}

Claude Desktop connects to remote servers from Anthropic's cloud, which cannot reach a server on your machine or a private network. Run the server over stdio from claude_desktop_config.json, which is ~/Library/Application Support/Claude/claude_desktop_config.json on macOS and %APPDATA%\Claude\claude_desktop_config.json on Windows:

{
  "mcpServers": {
    "adaptive-pg-mcp": {
      "command": "/usr/local/bin/adaptive-pg-mcp",
      "args": ["serve", "-config", "/etc/adaptive-pg-mcp/stdio.yaml"],
      "env": {
        "ADAPTIVE_PG_MCP_TOKEN": "apm_...",
        "HELPDESK_READ_DSN": "postgres://helpdesk_read:...@pg.example.internal:5432/helpdesk?sslmode=verify-full",
        "HELPDESK_WRITE_DSN": "postgres://helpdesk_write:...@pg.example.internal:5432/helpdesk?sslmode=verify-full"
      }
    }
  }
}

The file then holds the token and the passwords, so keep it readable by you alone. Restart Claude Desktop after a change.

Put this in .vscode/mcp.json, or in the user configuration that MCP: Open User Configuration opens. VS Code prompts for the token when the server first starts.

{
  "inputs": [
    {
      "type": "promptString",
      "id": "adaptive-pg-mcp-token",
      "description": "adaptive-pg-mcp token",
      "password": true
    }
  ],
  "servers": {
    "adaptive-pg-mcp": {
      "type": "http",
      "url": "https://mcp.example.internal:8443/mcp",
      "headers": {
        "Authorization": "Bearer ${input:adaptive-pg-mcp-token}"
      }
    },
    "adaptive-pg-mcp-stdio": {
      "type": "stdio",
      "command": "adaptive-pg-mcp",
      "args": ["serve", "-config", "/etc/adaptive-pg-mcp/stdio.yaml"],
      "env": {
        "ADAPTIVE_PG_MCP_TOKEN": "${input:adaptive-pg-mcp-token}"
      },
      "envFile": "/etc/adaptive-pg-mcp/dsn.env"
    }
  }
}

envFile names a file of NAME=value lines that holds the connection strings.

Put this in .cursor/mcp.json of the project, or in ~/.cursor/mcp.json for every project. ${env:NAME} reads a variable from the environment Cursor runs in.

{
  "mcpServers": {
    "adaptive-pg-mcp": {
      "url": "https://mcp.example.internal:8443/mcp",
      "headers": {
        "Authorization": "Bearer ${env:ADAPTIVE_PG_MCP_TOKEN}"
      }
    },
    "adaptive-pg-mcp-stdio": {
      "type": "stdio",
      "command": "adaptive-pg-mcp",
      "args": ["serve", "-config", "/etc/adaptive-pg-mcp/stdio.yaml"],
      "env": {
        "ADAPTIVE_PG_MCP_TOKEN": "${env:ADAPTIVE_PG_MCP_TOKEN}",
        "HELPDESK_READ_DSN": "${env:HELPDESK_READ_DSN}",
        "HELPDESK_WRITE_DSN": "${env:HELPDESK_WRITE_DSN}"
      }
    }
  }
}

Configuration

The server reads server.yaml, access.yaml in standalone mode, and the catalog. Each YAML file has version: 1. An unknown key, a missing required key, a value the server cannot use or a second YAML document fails the load with a sentence that names the file and what to fix. docs/DESIGN.md section 3 holds every rule, and examples/ holds files that the tests load.

server.yaml

version: 1
mode: standalone
listen: 127.0.0.1:8080
free_sql: read
limits: { rows: 1000, bytes: 1048576, timeout: 30s, write_rows: 100 }
access: ./access.yaml
catalog: ./catalog
databases:
  sales_eu:
    login: { dsn_env: SALES_EU_DSN }
  helpdesk:
    logins:
      read:  { dsn_env: HELPDESK_READ_DSN }
      write: { dsn_env: HELPDESK_WRITE_DSN }

Key

Required

What it sets

version

yes

The format version, 1.

mode

yes

standalone reads its callers from access.yaml. straza accepts only the tokens the Straza gateway holds.

listen

yes

stdio, or the host and port of Streamable HTTP, such as 127.0.0.1:8080.

tls

no

cert_file and key_file, the PEM files that HTTPS serves. See TLS.

allow_plain_http

no

true accepts plain HTTP on an address beyond this machine. Defaults to false.

free_sql

yes

off, read for query_sql, or read-write for query_sql, execute_sql and execute_ddl.

limits

yes

rows and bytes cap what a read returns, timeout is the statement_timeout of each call, and write_rows caps the rows a write may change.

access

standalone mode

The path of access.yaml. Refused in straza mode.

gateway_tokens_sha256

straza mode

The SHA-256 of each token Straza calls with. Refused in standalone mode.

listing_tokens_sha256

no

The SHA-256 of each token Straza sends only to list_databases, which is all such a token can call. Refused in standalone mode.

catalog

yes

The catalog folder, or none.

databases

yes

Each database under the name that calls use, with its logins.

Relative paths resolve against the folder of server.yaml. serve and check read ./server.yaml unless -config names another file.

A database has one login, or logins with read, write and any extra login that a catalog tool names, such as finance_reader. Each login names the environment variable that holds its connection string in dsn_env, so no password sits in the file. A connection string takes either libpq form, a URL or key=value pairs, such as postgres://helpdesk_read@pg.example.internal:5432/helpdesk?sslmode=verify-full. Logins are real logins, never roles to switch to, because free SQL can switch roles. One login for reads and writes works: reads still run in read-only transactions. Separate logins add the database's own grants as a second boundary.

Two keys per database relax a start check:

  • allow_cross_database: true accepts dblink or postgres_fdw in the database.

  • allow_superuser: true accepts a superuser login while free SQL is on.

Postgres logins

The server runs on logins that a DBA creates for it, and it creates none itself. A read login and a write login for the helpdesk database of the examples:

CREATE ROLE helpdesk_read LOGIN PASSWORD '...';
CREATE ROLE helpdesk_write LOGIN PASSWORD '...';

GRANT CONNECT ON DATABASE helpdesk TO helpdesk_read, helpdesk_write;
GRANT USAGE ON SCHEMA public TO helpdesk_read, helpdesk_write;

GRANT SELECT ON ALL TABLES IN SCHEMA public TO helpdesk_read;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO helpdesk_write;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO helpdesk_write;
  • Grants decide what agents see as well as what they change. The listing tools and describe_table show only the schemas, tables and columns that the login may use.

  • The write login needs SELECT next to the change grants, because Postgres checks it for the columns that a where filter or returning reads.

  • A column whose default calls nextval, such as a serial column, needs USAGE on its sequence.

  • GRANT ... ON ALL TABLES covers the tables that exist now. Use ALTER DEFAULT PRIVILEGES for tables created later.

  • execute_ddl runs on the write login, so it changes only what that login owns or may create. Grant CREATE on a schema only where agents should create tables.

  • Never give the server a superuser login while free SQL is on. The start refuses one, and reports a login that holds BYPASSRLS or may write to every table.

access.yaml

In standalone mode, access.yaml lists the callers:

version: 1
callers:
  - name: support_bot
    token_sha256: "<64 lowercase hex characters>"
    tools: [list_databases, list_tables, describe_table, read_rows, insert_rows]
    databases: [helpdesk]
    tables: [public.tickets]   # optional

tools and databases name what the caller may use. tables narrows the table tools to those tables, and a caller with tables may not use free SQL, because free SQL can reach any table. A catalog tool is listed in tools by its name.

adaptive-pg-mcp token prints a new token and its hash:

token: apm_...
token_sha256: 3f0c...

The token goes to the caller, and the hash goes into token_sha256. The server keeps neither, so the token shows only once.

The catalog

The catalog turns reviewed SQL into tools. It is a folder of .sql files, with one tools.yaml index per folder, at any depth. A statement starts at a -- name: line:

-- name: close_stale_tickets
UPDATE public.tickets
SET status = 'closed'
WHERE status = 'pending'
  AND updated_on < current_date - $1::int4
RETURNING id;

The index of its folder makes it a tool:

version: 1
tools:
  close_stale_tickets:
    description: Closes the tickets that have waited in pending for more than a number of days. Returns the id of each ticket it closed.
    mode: write                    # or read
    databases: [helpdesk]
    login: write                   # read, write, or an extra login
    params:                        # in $1 order
      - name: days
        description: How many days a ticket waits in pending before it closes, from 7 to 90.
        required: true             # also enum, pattern, minimum, maximum, maxLength
        minimum: 7
        maximum: 90
    limits: { write_rows: 50 }     # optional: rows, bytes, timeout, write_rows

Postgres reports the type of each parameter when the server starts, and the tool's input schema follows it. A parameter may be of type bool, int2, int4, int8, float4, float8, numeric, text, varchar, bpchar, uuid, date, timestamp, timestamptz, json or jsonb, or of a domain over one of them. A parameter that a call leaves out binds NULL. A read tool runs in a read-only transaction, and a write tool is rolled back when it changes more rows than its cap or changes structure. Each field of an entry's limits replaces the same field of server.yaml for that tool, and the start reports each cap an entry raises.

The catalog skips files and folders whose name starts with a dot, so it loads from a Kubernetes ConfigMap mount. It follows a link only when the link is relative and stays inside the catalog folder.

Writing catalog tools

Agents never read this README. When a client connects, it fetches the tool list, and the agent picks a tool by its name, its description, the description, type and limits of each parameter, and whether it reads or writes. The description in tools.yaml is the agent's documentation, so say what the tool does, what it returns and its caps, in words an agent can act on. The start refuses a tool or a parameter without a description.

Each tool is exactly one SQL statement, because the server prepares it as one statement. A script of several statements, such as one wrapped in BEGIN and COMMIT, or a psql command such as \copy does not prepare, and the start refuses it. Parameters are $1, $2 and so on, in the order of params. psql variables such as :month do not work. A parameter of a type outside the list above needs a cast in the statement, such as $1::text.

One .sql file may hold many statements, each from its -- name: line to the next one. A statement that no tools.yaml lists is not a tool, and the start reports it. A tools.yaml entry without a statement of its name refuses the start.

The server reads the catalog once, at start, so a changed file takes effect after a restart. adaptive-pg-mcp check prepares every statement on its databases and runs the grants check without serving, so CI can run it against a staging database before a deploy.

Running a catalog in production

A catalog fits in a git repository of its own, next to access.yaml:

access.yaml
catalog/
  helpdesk/
    tickets.sql
    tools.yaml
  sales/
    revenue.sql
    tools.yaml
  1. Every change is a pull request, and a code owners rule makes a DBA review each statement and its description.

  2. CI runs adaptive-pg-mcp check against a staging database with the same schema, so a statement that does not prepare or lacks a grant fails the pull request.

  3. A merge deploys the files, and the server loads them at its next start.

examples/kubernetes makes one ConfigMap per catalog folder, because a ConfigMap key holds no folder. Kustomize puts a hash of each ConfigMap's content in its name, so a changed file rolls the Deployment. A ConfigMap holds at most 1 MiB, so a large catalog does not fit in one. Build an image of your own on this one instead, which also versions the SQL with the image:

FROM ghcr.io/strazahq/adaptive-pg-mcp:0.1.1
COPY catalog /usr/share/adaptive-pg-mcp/catalog

Then set catalog: /usr/share/adaptive-pg-mcp/catalog in server.yaml. The path stays outside /etc/adaptive-pg-mcp, so the mount that brings server.yaml and access.yaml does not hide it.

Tools

Every tool except list_databases takes a required database argument, whose schema lists the databases the caller may use. Where a tool takes a table, schema defaults to public. Results carry structuredContent that matches the tool's output schema, and a short text. A bad argument or a refusal returns a tool result with isError: true and a sentence that says what to change.

Tool

What it does

Login

Kind

list_databases

Lists the configured databases with their ids, 25 per page.

read

read

list_schemas

Lists the schemas the login may use, up to 100 per page.

read

read

list_tables

Lists the tables, views and foreign tables the login may use, in one schema or all, up to 100 per page.

read

read

describe_table

Returns the columns of a table or view: name, type, nullable, default and primary key.

read

read

read_rows

Returns rows, with a column list, where filters, order_by and limit.

read

read

aggregate

Returns count, sum, avg, min or max of a column, with filters and group_by.

read

read

insert_rows

Inserts rows and returns rowsAffected and the returning columns.

write

write

update_rows

Updates the rows a required where matches.

write

write

delete_rows

Deletes the rows a required where matches.

write

write

query_sql

Runs one SQL statement in a read-only transaction. Needs free_sql: read or read-write.

read

read

execute_sql

Runs one statement that changes rows, such as INSERT, UPDATE, MERGE or CALL, under the write cap. Refuses TRUNCATE and changes of structure. Needs free_sql: read-write.

write

write

execute_ddl

Runs TRUNCATE or one statement that changes structure. Refuses a statement that changes rows only. Needs free_sql: read-write.

write

write

one per catalog statement

Runs the statement with the call's arguments as its parameters.

its entry's

its entry's

A where filter is a list of {column, op, value}, with op one of =, <>, <, <=, >, >=, in, not in, is null, is not null, like and ilike. A read returns columns with their Postgres types, rows as arrays in column order, and truncated. Numbers and booleans are JSON numbers and booleans, json is JSON, dates and timestamps are ISO 8601 with timestamptz in UTC, and other types are their Postgres text.

Read tools set readOnlyHint, and write tools set destructiveHint.

Start checks

serve runs the start checks before it serves, and check runs them without serving. They connect every login, and these findings refuse the start:

  • A login is a superuser, or a member of a superuser role, while free SQL is on, and its database does not set allow_superuser: true.

  • A database that one of its logins reaches has dblink or postgres_fdw installed, and the database does not set allow_cross_database: true.

  • A login of a database reaches another database than its read login, compared by the cluster's system identifier and the database OID, or the two cannot be compared.

  • A catalog statement does not prepare on one of its databases, takes a different number of parameters than its index lists, needs a grant that its login lacks, fails to plan for a reason that its NULL parameters cannot cause, or has a parameter whose type differs between two of its databases.

  • A login cannot connect, the catalog does not load, or the TLS certificate and key do not load or the certificate has expired.

The grant check prepares each catalog statement and plans it with EXPLAIN (ANALYZE false) and NULL for each parameter, on its entry's login in a read-only transaction that is rolled back. It runs no statement. A few grants show only on the first call, such as USAGE on a sequence that a column default calls.

Everything else is reported: what each login can do, such as writing to every table or holding BYPASSRLS, a statement whose grants EXPLAIN cannot check, each cap a catalog entry raises, and each tool in access.yaml that the server does not serve.

$ adaptive-pg-mcp check -config /etc/adaptive-pg-mcp/server.yaml
report: ...
check: no refusals, so adaptive-pg-mcp would start.

check prints one refusal: or report: line per finding and a closing check: line, and exits 1 when serve would refuse to start. It needs the same connection strings as serve, so a catalog repository can run it in its CI against the real databases. serve logs refusals at ERROR and reports at WARN, on stderr.

TLS

With tls, Streamable HTTP serves HTTPS only:

listen: 0.0.0.0:8443
tls:
  cert_file: /etc/adaptive-pg-mcp/tls/tls.crt   # the certificate, then any intermediates
  key_file: /etc/adaptive-pg-mcp/tls/tls.key

It accepts TLS 1.2 and later with Go's default cipher suites, offers HTTP/2 and HTTP/1.1, and asks for no client certificate. A pair that does not load, or a certificate that has expired, refuses the start. While it serves, the server reads both files again at a handshake once a minute has passed since the last read. A tool such as cert-manager that replaces them renews the pair without a restart, and a new pair that does not load leaves the old one in service with a warning.

Without tls, listen must be localhost or a loopback address. Any other address, 0.0.0.0 included, fails the load unless allow_plain_http: true says that something in front of the server encrypts the traffic, such as a service mesh. The start then reports that bearer tokens cross the network in cleartext.

Behind the Straza gateway

In straza mode the server trusts the gateway for identity and policy:

mode: straza
gateway_tokens_sha256:
  - "<SHA-256 of the token Straza calls with>"
listing_tokens_sha256:
  - "<SHA-256 of the token Straza lists with>"

The server accepts only those tokens. A gateway token reaches every tool, and every request with it has the caller straza. A listing token sees and calls list_databases only, so a leaked one cannot read or change data. A hash in both lists fails the load, and a listing token whose hash is in gateway_tokens_sha256 alone would reach every tool, so keep it in listing_tokens_sha256. Straza decides which tools and which databases each agent may use, holds a call for approval when its policy says so, and writes the audit record. Each data tool carries the mark ai.straza/object: {type: database, argument: database}, and list_databases lists the databases for Straza with an id that joins the cluster's system identifier, the database OID and the config name. When a call's _meta carries ai.straza/object-id, the server compares it with the id read on the call's own connection and refuses a mismatch, so a call on a database dropped and created again under the same name is refused until Straza lists the databases again.

Docker and Kubernetes

The image reads /etc/adaptive-pg-mcp/server.yaml. Mount a folder that holds server.yaml, access.yaml and the catalog there, and pass each connection string as an environment variable. Inside a container, listen needs an address that the host can reach, such as 0.0.0.0:8443, and with it tls or allow_plain_http: true.

docker run --rm -p 127.0.0.1:8443:8443 \
  -v /srv/adaptive-pg-mcp:/etc/adaptive-pg-mcp:ro \
  -e HELPDESK_READ_DSN -e HELPDESK_WRITE_DSN \
  ghcr.io/strazahq/adaptive-pg-mcp:0.1.1

The same image makes tokens, runs the checks, and serves stdio for a client that starts it:

docker run --rm ghcr.io/strazahq/adaptive-pg-mcp:0.1.1 token
docker run --rm -v /srv/adaptive-pg-mcp:/etc/adaptive-pg-mcp:ro \
  -e HELPDESK_READ_DSN -e HELPDESK_WRITE_DSN \
  ghcr.io/strazahq/adaptive-pg-mcp:0.1.1 check -config /etc/adaptive-pg-mcp/server.yaml
docker run -i --rm -v /srv/adaptive-pg-mcp:/etc/adaptive-pg-mcp:ro \
  -e ADAPTIVE_PG_MCP_TOKEN -e HELPDESK_READ_DSN -e HELPDESK_WRITE_DSN \
  ghcr.io/strazahq/adaptive-pg-mcp:0.1.1 serve -config /etc/adaptive-pg-mcp/stdio.yaml

examples/kubernetes holds manifests for kubectl apply -k. They need cert-manager and an issuer.

  • The Deployment runs one replica as uid 65532, with a read-only root file system, no privilege escalation, every capability dropped and the RuntimeDefault seccomp profile, which the restricted Pod Security Standard of its namespace enforces.

  • server.yaml and access.yaml, and each catalog folder, become ConfigMaps whose names carry a hash of their content, so a change rolls the Deployment. The server reads them only at start.

  • A projected volume puts the ConfigMaps and the cert-manager Secret into one folder at the paths server.yaml names. cert-manager renews the pair in place, and the server serves the new pair without a restart.

  • The connection strings come from a Secret through the environment. The Secret is created outside the manifests, as kustomization.yaml shows.

  • The probes open a TCP connection, because every HTTP request needs a token.

  • Clients on revisions before 2026-07-28 keep a protocol session in the memory of one replica, so more replicas need session affinity in front.

A test loads the example's server.yaml, access.yaml and catalog, and checks that the manifests deliver each connection string and catalog file.

Development

make hooks    # bind the git hooks in .githooks, once per clone
make check    # text, state and Go gates, gofmt, go vet and go test
make pgtest   # the database tests on Postgres 14 to 18 in local Docker
make security # gitleaks, govulncheck, osv-scanner and the spec gate

go test ./... needs no database. Tests that touch one carry the build tag pg, and make pgtest runs them against one container per major version, which tools/pg.sh starts and removes. The push gate runs the security scans and the database tests. The workflow in .github/workflows/image.yml runs go vet, go test and govulncheck, and then builds and publishes the image.

docs/DESIGN.md is the design, docs/PLAN.md holds the decisions and invariants, and CHANGELOG.md records each change.

License

Apache License 2.0. See LICENSE and NOTICE.

Available Tools

12 tools
aggregateA
Read-onlyIdempotent

Computes count, sum, avg, min or max over one table or view, with an optional filter and grouping. Returns at most 1,000 groups.

NameRequiredDescriptionDefault
tableYesThe table or view to aggregate, by exact name.
whereNoConditions joined by AND. Omit to aggregate every row.
columnNoThe column to aggregate. Required except for count, which counts rows without it.
schemaNoThe schema, by exact name. Defaults to public.
databaseYesThe database to run on.
functionYesThe aggregate function.
group_byNoColumns to group and order by. Omit for one row.

Output Schema

NameRequiredDescription
rowsYesThe result rows, each an array in column order.
columnsYesThe result columns, in order.
truncatedYesTrue when the row cap, the byte cap or limit cut off more rows.

TDQS

A3.8/5.0
Behavior4/5

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

Annotations already declare readOnly, idempotent, non-destructive, closed-world behavior, so the safety profile is covered. The description adds a genuine trait not present in the annotations or schema: results are capped at 1,000 groups, which matters for planning grouped queries. It stops short of describing ordering or behavior when the cap is hit.

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

Conciseness5/5

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

Two sentences, zero waste, with the operation stated first and the result cap front-loaded as the second point. Nothing is padded or repeated.

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?

With an output schema present and full parameter coverage, the description only needs to frame the operation and any behavioral limits, which it does via the 1,000-group cap. It could go further on result ordering or the relationship to query_sql, but nothing essential for a correct call is missing.

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

Parameters3/5

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

Schema description coverage is 100%, so every parameter, including the where-clause operators and the count-without-column case, is already documented in the schema. The description restates the function list and the optional filter/grouping at a high level without adding syntax or edge-case meaning. Baseline 3 applies.

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 verb ('Computes') and the exact resource (count/sum/avg/min/max over one table or view), so the operation is unambiguous. It does not, however, distinguish itself from siblings like query_sql or execute_sql, which an agent could reasonably consider for the same task.

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 only implied: the mention of 'one table or view' plus optional filter and grouping suggests when the tool fits, but there is no explicit 'use this instead of query_sql when you need aggregates' routing. No prerequisites, exclusions, or alternatives are named.

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

delete_rowsA
Destructive

Deletes the rows of one table that match a where filter. Rolls back a call that changes more than 100 rows, triggers and cascades included. Returns rowsAffected and up to 1,000 returning rows.

NameRequiredDescriptionDefault
tableYesThe table or view to delete from, by exact name.
whereYesConditions joined by AND that pick the rows to delete.
schemaNoThe schema, by exact name. Defaults to public.
databaseYesThe database to run on.
returningNoColumns to return from each changed row. Omit to return none.

Output Schema

NameRequiredDescription
rowsYesThe result rows, each an array in column order.
columnsYesThe result columns, in order.
rowsAffectedYesThe number of rows changed.

TDQS

A4/5.0
Behavior4/5

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

Goes well beyond the annotations by disclosing the automatic rollback above 100 affected rows and that triggers and cascades are included in that count – a critical guard an agent cannot infer from destructiveHint=true alone. It also previews the return shape (rowsAffected plus up to 1,000 returning rows).

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?

Three short sentences, front-loaded with the action and filter, then the safety guard, then the return contract. No filler and no repetition of the name or title.

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?

An output schema exists, so return values need not be fully explained, and the description still summarizes them. The only minor gap is that it never mentions permissions or which alternative tools overlap with this mutation.

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

Parameters3/5

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

Schema description coverage is 100%, so the parameters are already fully documented and the description only echoes 'where filter' and the returning cap. It adds no syntax or semantics the schema does not cover, so the baseline 3 applies.

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 a specific verb and resource with precise scope: 'Deletes the rows of one table that match a where filter.' That framing distinguishes it from update_rows, read_rows and insert_rows without needing to name them, and rules out multi-table or unfiltered deletion.

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 by the verb and the required where clause, but there is no explicit guidance such as when to prefer update_rows over delete+insert, or when execute_sql is the better route. The 100-row rollback is a safety behavior rather than a when-to-use rule.

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

describe_tableA
Read-onlyIdempotent

Describes the columns of one table or view that the read login may use: name, type, nullable, default and primary key.

NameRequiredDescriptionDefault
tableYesThe table or view to describe, by exact name.
schemaNoThe schema, by exact name. Defaults to public.
databaseYesThe database to run on.

Output Schema

NameRequiredDescription
rowsYesThe result rows, each an array in column order.
columnsYesThe result columns, in order.
truncatedYesTrue when the row cap, the byte cap or limit cut off more rows.

TDQS

A3.8/5.0
Behavior4/5

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

Annotations already declare readOnly, idempotent, non-destructive and closed-world, so the safety profile is covered. The description adds a genuine behavioral trait beyond them: only objects visible to the read login are described, so absence of a table is not proof it does not exist. It does not discuss behavior for a nonexistent table or ambiguous schema, but that is a minor gap given the annotation coverage.

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 verb and scope and then lists the returned attributes, with no filler or redundant restatement of the tool name.

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

Completeness4/5

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

The tool is simple, parameters are fully documented, an output schema covers the return shape, and annotations cover safety, so the description is nearly sufficient as-is. The only missing piece is guidance on failure or not-found behavior and how it relates to sibling introspection tools.

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%, with the table, schema and database parameters each documented, including the public schema default and the single-value database enum. The description adds only the clarification that the table argument may also be a view, which the schema already states, so baseline 3 applies.

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?

States a specific verb (describes) and resource (columns of one table or view) and enumerates the returned attributes: name, type, nullable, default, primary key. The singular scope distinguishes it implicitly from list_tables/list_schemas, but no sibling is named explicitly.

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 only implied: an agent can infer this is the follow-up step to list_tables when column-level detail is needed. There is no explicit when-to-use statement, no exclusion such as 'do not use to fetch row data – use read_rows', and no prerequisite about knowing the exact table name.

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

execute_ddlA
Destructive

Runs one DDL statement, such as CREATE, ALTER or DROP, or a TRUNCATE. Rolls back a statement that only works on rows, which needs execute_sql, and one that changes more than 100 rows in tables that existed before it. Returns at most 1,000 rows. Times out after 30s.

NameRequiredDescriptionDefault
sqlYesOne SQL statement, sent to Postgres unchanged. More than one statement is refused.
databaseYesThe database to run on.

Output Schema

NameRequiredDescription
rowsYesThe result rows, each an array in column order.
columnsYesThe result columns, in order.
rowsAffectedYesThe number of rows changed.

TDQS

A4.6/5.0
Behavior5/5

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

Annotations already declare destructiveHint=true and idempotentHint=false, and the description goes well beyond them: rollback semantics for disallowed statements, a 100-row mutation ceiling applied only to pre-existing tables, a 1,000-row return cap, and a 30s timeout. These are exactly the operational limits an agent needs before firing a destructive call.

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?

Four short sentences, front-loaded with the operation type before limits and caveats, with no filler. The rollback sentences are slightly convoluted, especially 'one that changes more than 100 rows in tables that existed before it', which takes a reread to parse.

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?

An output schema exists, so return values need no explanation; the description instead covers the operational envelope (timeout, row caps, rollback guards) that determines whether the call succeeds. For a destructive two-parameter tool, nothing critical is missing.

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

Parameters3/5

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

Schema coverage is 100%, so the single-statement constraint, the database enum, and the 'sent to Postgres unchanged' note are all already in the schema. The description adds no parameter-specific detail, so the baseline 3 applies.

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?

Names the exact operation family ('one DDL statement') and enumerates concrete examples (CREATE, ALTER, DROP, TRUNCATE), which immediately separates it from row-oriented siblings. An agent can tell this is for schema changes, not data changes, without opening the schema.

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

Usage Guidelines5/5

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

Explicitly routes row-only statements to execute_sql and states the two rejection conditions (row-only statements, mutations exceeding 100 rows in pre-existing tables). This is real when-to-use/when-not-to-use guidance tied to a named alternative.

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

execute_sqlA
Destructive

Runs one SQL statement that works on rows, such as INSERT, UPDATE, DELETE, MERGE, CALL or DO. Rolls back TRUNCATE and changes of structure, which need execute_ddl, and a statement that changes more than 100 rows across all tables. Returns at most 1,000 rows. Times out after 30s.

NameRequiredDescriptionDefault
sqlYesOne SQL statement, sent to Postgres unchanged. More than one statement is refused.
databaseYesThe database to run on.

Output Schema

NameRequiredDescription
rowsYesThe result rows, each an array in column order.
columnsYesThe result columns, in order.
rowsAffectedYesThe number of rows changed.

TDQS

A4.3/5.0
Behavior5/5

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

Annotations already declare destructive/not-readonly, so the description's job is to add operational detail, and it does: statements over 100 affected rows across all tables are rejected, results are capped at 1,000 rows, and execution times out at 30s. Those are exactly the constraints an agent needs before composing a statement, and none of them are in the annotations or 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?

Four short sentences, front-loaded with the core action, then constraints in descending order of importance (DDL exclusion, row-impact cap, result cap, timeout). No filler and nothing repeated.

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?

With an output schema present, return-value documentation is unnecessary, and the description still supplies limits, timeouts, and rejection behavior, so it is nearly complete for a single-parameter mutation tool. The one residual gap is that 'Rolls back TRUNCATE...' does not explicitly say the statement is refused/aborted rather than silently attempting and reverting, which could be read either way.

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

Parameters4/5

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

Schema description coverage is 100% and both parameters are already documented ('One SQL statement, sent to Postgres unchanged'; the database enum). The description reinforces the single-statement constraint ('Runs one SQL statement') but adds no new syntax, dialect, or format guidance beyond the schema.

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 opens with a specific verb and resource ('Runs one SQL statement that works on rows') and enumerates the statement families it accepts (INSERT, UPDATE, DELETE, MERGE, CALL, DO), which is far more than a restatement of the name. It cleanly distinguishes itself from execute_ddl. It does not, however, distinguish itself from query_sql for reads or from insert_rows/update_rows/delete_rows for single-row work, leaving some sibling ambiguity.

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?

It gives one explicit routing rule: TRUNCATE and structure changes 'need execute_ddl', which tells the agent precisely when to pick a different tool. It also implies a row-count boundary (>100 rows across all tables is refused) as a reason to fall back to other tools. It stops short of naming query_sql as the read alternative or the row-level CRUD siblings as safer options.

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

insert_rowsA
Destructive

Inserts rows into one table. Rolls back a call that changes more than 100 rows, triggers and cascades included. Returns rowsAffected and up to 1,000 returning rows.

NameRequiredDescriptionDefault
rowsYesThe rows to insert. An omitted column takes its default.
tableYesThe table or view to insert into, by exact name.
schemaNoThe schema, by exact name. Defaults to public.
databaseYesThe database to run on.
returningNoColumns to return from each changed row. Omit to return none.

Output Schema

NameRequiredDescription
rowsYesThe result rows, each an array in column order.
columnsYesThe result columns, in order.
rowsAffectedYesThe number of rows changed.

TDQS

A3.9/5.0
Behavior4/5

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

Annotations already flag destructiveHint=true and the absence of idempotency, so the safety profile is largely covered. The description adds genuinely new behavioral facts: a hard rollback threshold at 100 affected rows, that triggers and cascades count toward that limit, and that the call returns rowsAffected plus capped returning rows.

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?

Three sentences, zero filler, with the operation stated first and the consequential rollback limit immediately after. Nothing here could be removed without losing information.

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?

An output schema exists, so return shape doesn't need explaining, and the 100% parameter coverage handles the inputs. What remains — the silent rollback guardrail, trigger/cascade counting, and single-table restriction — is exactly the non-obvious information an agent needs before calling a destructive write.

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

Parameters3/5

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

Schema description coverage is 100%, so every parameter including rows, table, schema, database, and returning is already documented with defaults. The description adds only the 1,000-row cap on the returning output, which is a marginal contribution beyond the schema's baseline.

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?

States a specific verb and resource ("Inserts rows into one table") and bounds scope to a single table, which separates it in practice from update_rows, delete_rows, and the free-form execute_sql. It never names those siblings explicitly, which keeps it short of a 5.

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 by the verb: use this to add rows rather than to modify or remove them. There is no explicit when-to-use or when-not guidance, and no steer toward execute_sql for cases this tool can't express (e.g. multi-table inserts).

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

list_databasesA
Read-onlyIdempotent

Lists the databases the caller may use, 25 per page. Pass an object's value as database to the other tools.

NameRequiredDescriptionDefault
cursorNoThe nextCursor of the previous page. Omit for the first page.

Output Schema

NameRequiredDescription
objectsYesThe databases on this page.
nextCursorNoSet when more follow. Pass it as cursor for the next page.

TDQS

A3.8/5.0
Behavior4/5

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

Annotations already declare readOnly, idempotent, non-destructive and closed-world, so the safety profile is covered. The description adds two things annotations cannot: results are permission-scoped to the caller and paginated at 25 per page, which directly affects how an agent must loop. It does not mention termination conditions or total counts, but an output schema exists to cover return shape.

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

Conciseness5/5

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

Two sentences, zero filler, and the core identity (what is listed and under what access scope) is front-loaded before the pagination and hand-off details. Every clause carries information an agent needs.

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 one-parameter, read-only discovery tool with an output schema, the description covers access scope, page size, and how to reuse the returned values downstream. It is nearly complete; only the lack of explicit ordering relative to the other list_* tools leaves a small gap.

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

Parameters3/5

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

Schema coverage is 100% and the single cursor parameter is already documented as 'the nextCursor of the previous page.' The description's '25 per page' adds useful page-size context that shapes cursor looping, but it adds no new meaning about the cursor parameter itself. Baseline 3 is appropriate.

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?

States a specific verb and resource ('Lists the databases') plus an access qualifier ('the caller may use'), which cleanly separates it from list_schemas and list_tables in the sibling set. It stops short of naming those siblings explicitly, so an agent must infer the hierarchy rather than being told.

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 second sentence gives downstream usage ('Pass an object's value as database to the other tools'), which is genuinely useful routing guidance. However, it never states when to call this versus list_schemas/list_tables, nor that this is the entry point before schema or table discovery — that ordering is only implied.

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

list_schemasA
Read-onlyIdempotent

Lists the schemas that the read login may use, without the system schemas, 100 per page.

NameRequiredDescriptionDefault
cursorNoThe nextCursor of the previous list_schemas page. Omit for the first page.
databaseYesThe database to run on.

Output Schema

NameRequiredDescription
rowsYesThe result rows, each an array in column order.
columnsYesThe result columns, in order.
truncatedYesTrue when the row cap, the byte cap or limit cut off more rows.
nextCursorNoSet when more follow. Pass it as cursor for the next page.

TDQS

A4/5.0
Behavior4/5

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

Annotations already cover readOnly, idempotent, non-destructive, and closed-world, so the safety profile is done. The description still earns credit by disclosing behavior the annotations do not: results are privilege-filtered to the read login, system schemas are suppressed, and pages are capped at 100 rows.

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 front-loaded sentence carrying the scope filter and the page-size constraint with zero filler. Nothing to trim and nothing important buried.

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?

Appropriate for a simple two-parameter listing tool: an output schema handles return shape and annotations handle safety, so the description only needs scope and pagination, which it supplies. Minor gap: no mention of result ordering or what happens when the cursor expires.

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 both parameters (cursor with first-page guidance, database with an enum) are documented in the schema itself. The description's "100 per page" gives indirect context for pagination but adds no per-parameter meaning beyond what the schema already states.

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 a specific verb and resource ("Lists the schemas") plus two scope qualifiers that matter operationally: only schemas the read login may access, and system schemas excluded. The resource name alone separates it from siblings like list_tables and list_databases, so an agent can route without opening the schema.

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 by the name and scope (use to enumerate schemas before describing or querying tables), but there is no explicit when-to-use statement, no named alternative, and no note about whether to call this before list_tables. Adequate but relies on inference.

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

list_tablesA
Read-onlyIdempotent

Lists the tables, views, materialized views and foreign tables that the read login may use, with schema, name and kind, 100 per page.

NameRequiredDescriptionDefault
cursorNoThe nextCursor of the previous list_tables page. Omit for the first page.
schemaNoThe schema to list, by exact name. Omit for every schema.
databaseYesThe database to run on.

Output Schema

NameRequiredDescription
rowsYesThe result rows, each an array in column order.
columnsYesThe result columns, in order.
truncatedYesTrue when the row cap, the byte cap or limit cut off more rows.
nextCursorNoSet when more follow. Pass it as cursor for the next page.

TDQS

A3.8/5.0
Behavior4/5

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

Annotations already declare readOnly, idempotent, non-destructive and closed-world, so the safety profile is covered. The description adds meaningful context beyond that: results are permission-scoped to the read login, and results are paginated at 100 per page. It stops short of stating ordering, total counts, or whether system schemas appear.

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 zero filler; the resource scope and the returned fields are front-loaded, with the pagination detail trailing. Nothing is repeated from the name or title, and nothing could be cut without losing information.

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?

An output schema exists, so return-shape explanation is unnecessary, and the description still names the key returned fields. Pagination and the permission scope are both covered for a low-complexity, three-parameter read tool. Only result ordering and default schema behavior are left unstated, which is minor.

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

Parameters3/5

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

Schema coverage is 100%, so each parameter including cursor, schema and database is already documented in the schema. The description's only added semantic is the 100-per-page pagination cadence, which implies the cursor workflow but does not explain it further. Baseline 3 applies when the schema does the heavy lifting.

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 specific verb ('Lists') with a precisely enumerated resource: tables, views, materialized views and foreign tables. It also states the returned fields (schema, name, kind) and the page size. It does not explicitly contrast itself with siblings like list_schemas or describe_table, leaving that inference to the caller.

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 only implied: an agent can infer this is the discovery step before describe_table or read_rows, and that it lists only objects visible to the read login. There is no explicit when-to-use, when-not-to-use, or named alternative (e.g. list_schemas for schema-level discovery).

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

query_sqlA
Read-onlyIdempotent

Runs one SQL statement in a read-only transaction, so it cannot change data. Returns at most 1,000 rows and 1,048,576 bytes. Times out after 30s.

NameRequiredDescriptionDefault
sqlYesOne SQL statement, sent to Postgres unchanged. More than one statement is refused.
databaseYesThe database to run on.

Output Schema

NameRequiredDescription
rowsYesThe result rows, each an array in column order.
columnsYesThe result columns, in order.
truncatedYesTrue when the row cap, the byte cap or limit cut off more rows.

TDQS

A3.6/5.0
Behavior4/5

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

The annotations already declare readOnlyHint, idempotentHint, and destructiveHint=false, so the safety profile is structured. The description still adds behavior annotations cannot express: a 1,000-row cap, a 1,048,576-byte cap, and a 30s timeout — exactly the operational limits an agent needs to anticipate truncated results. It stops short of saying whether hitting a cap truncates silently or errors, which is the one missing piece.

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?

Three short sentences, front-loaded with the operation and its safety guarantee, followed by concrete limits. No filler and nothing repeated from the title or schema verbatim.

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?

An output schema exists, so return values need not be explained, and the description covers the mutation-safety question and the resource caps. Relative to a two-parameter query tool that is nearly complete; only the truncation-vs-error behavior at the row/byte limits 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 coverage is 100%, so both parameters are already documented, including the single-statement constraint and the database enum. The description's "one SQL statement" and the read-only framing add emphasis but no meaning beyond the schema, so the baseline 3 applies.

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?

States a specific verb and resource ("Runs one SQL statement") plus a scope qualifier ("in a read-only transaction"), which is much stronger than a name restatement. It does not distinguish itself from the near-identical siblings execute_sql and execute_ddl, nor from aggregate/read_rows, so an agent cannot tell from the description alone which of those overlapping tools to pick.

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?

"so it cannot change data" implies the tool is for read queries, but there is no explicit when-to-use instruction. With eleven siblings including execute_sql, execute_ddl, aggregate, and read_rows, the absence of any routing guidance (e.g. "use aggregate for GROUP BY summarization") is a real gap.

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

read_rowsA
Read-onlyIdempotent

Reads rows of one table or view, with an optional filter, order and limit. Returns at most 1,000 rows and 1,048,576 bytes.

NameRequiredDescriptionDefault
limitNoThe most rows to return. Defaults to the row cap of 1,000.
tableYesThe table or view to read, by exact name.
whereNoConditions joined by AND. Omit to read every row.
schemaNoThe schema, by exact name. Defaults to public.
columnsNoColumns to return, in order. Omit for every column the login may select.
databaseYesThe database to run on.
order_byNoThe sort order, first entry first.

Output Schema

NameRequiredDescription
rowsYesThe result rows, each an array in column order.
columnsYesThe result columns, in order.
truncatedYesTrue when the row cap, the byte cap or limit cut off more rows.

TDQS

A3.6/5.0
Behavior4/5

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

Annotations already declare readOnly, idempotent, non-destructive and closed-world, so the safety profile is covered. The description adds genuinely useful behavioral facts the annotations lack: hard caps of 1,000 rows and 1,048,576 bytes. It does not say whether hitting those caps truncates silently or raises an error, which keeps it below a 5.

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

Conciseness5/5

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

Two sentences, zero filler, with the operation stated first and the return-size ceiling second. Nothing redundant or buried.

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?

With an output schema present, return values need no explanation, and all seven parameters are fully described in the schema. The remaining gap is behavioral edge cases around the row/byte caps and the permission scoping implied by "every column the login may select."

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

Parameters3/5

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

Schema description coverage is 100%, so the baseline is 3, and the description only echoes the schema ("optional filter, order and limit"). It adds no extra meaning about default schema, column projection, or operator behavior beyond what the schema already documents.

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 gives a specific verb and resource ("Reads rows of one table or view") plus the three optional capabilities (filter, order, limit), so an agent immediately knows the shape of the operation. It stops short of naming a sibling such as query_sql or aggregate, so differentiation is left to inference.

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 statement of when to reach for this tool over query_sql, execute_sql, aggregate, or the write siblings like insert_rows/update_rows. The only conditional guidance ("Omit to read every row") is about a parameter default rather than tool selection.

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

update_rowsA
Destructive

Updates the rows of one table that match a where filter. Rolls back a call that changes more than 100 rows, triggers and cascades included. Returns rowsAffected and up to 1,000 returning rows.

NameRequiredDescriptionDefault
setYesThe columns to set and their new values.
tableYesThe table or view to update, by exact name.
whereYesConditions joined by AND that pick the rows to update.
schemaNoThe schema, by exact name. Defaults to public.
databaseYesThe database to run on.
returningNoColumns to return from each changed row. Omit to return none.

Output Schema

NameRequiredDescription
rowsYesThe result rows, each an array in column order.
columnsYesThe result columns, in order.
rowsAffectedYesThe number of rows changed.

TDQS

A4/5.0
Behavior5/5

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

Adds real behavioral context beyond the annotations: an automatic rollback when more than 100 rows would change, and the warning that triggers and cascades are included in that count. This is exactly the kind of side-effect disclosure an agent needs before a destructive mutation, and it complements rather than repeats destructiveHint=true.

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?

Three short sentences with zero filler: scope first, safety guard second, return contract third. Every clause carries information an agent would otherwise have to discover by trial.

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?

With an output schema present and rich annotations, the description covers the essentials: mutation scope, the rollback threshold, cascade behavior, and return shape. Minor omissions such as transaction/atomicity semantics or permission requirements keep it from being 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 100%, so the parameters are already well documented. The description adds only one meaningful detail beyond the schema — the 1,000-row cap on 'returning' — which is useful but limited; the baseline of 3 applies.

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?

States a specific verb and resource ('Updates the rows of one table that match a where filter'), scoping it to a single table and a filter predicate. It does not name or distinguish itself from siblings like execute_sql or insert_rows, so the agent must infer the boundary from the name alone.

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 usage through the 'where filter' phrasing but never states when to choose this over execute_sql or execute_ddl, nor any prerequisites. The when-to-use case is inferable from the tool name but not spelled out.

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. 12 tool updates
    • First observedaggregate
    • First observeddelete_rows
    • First observeddescribe_table
    • First observedexecute_ddl
    • First observedexecute_sql
    • First observedinsert_rows
    • First observedlist_databases
    • First observedlist_schemas
    • First observedlist_tables
    • First observedquery_sql
    • First observedread_rows
    • First observedupdate_rows

TDQS

A3.9/5.0

Scored across 12 tools

Disambiguation4/5

Each tool has a stated purpose and the descriptions explicitly carve boundaries (execute_sql rejects TRUNCATE/structure changes; execute_ddl rejects row-only statements; query_sql is read-only). The main overlap is execute_sql versus insert_rows/update_rows/delete_rows and query_sql versus read_rows/aggregate, where an agent could reasonably pick either, though the specialized tools are clearly the safer/convenience option.

Naming Consistency4/5

Nearly all names follow a verb_noun pattern (list_tables, read_rows, insert_rows, update_rows, delete_rows, execute_sql, describe_table), which is predictable and readable. The only deviation is the bare verb 'aggregate', which lacks a noun target.

Tool Count5/5

Twelve tools is a well-scoped surface for a PostgreSQL server: discovery (list_databases/schemas/tables), inspection (describe_table), reads (read_rows, query_sql, aggregate), and writes (insert/update/delete/execute_sql/execute_ddl). No tool appears redundant enough to drop.

Completeness4/5

CRUD, DDL, arbitrary read SQL, aggregation, and schema/database discovery are all covered, so core lifecycle workflows have no dead ends. Gaps remain for transaction control (begin/commit/rollback across statements) and query plan introspection (EXPLAIN), and pagination beyond the first page is only implied for the list_* tools.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    An open-source MCP server for PostgreSQL schema introspection and guarded read-only queries. It enables MCP clients to discover schemas, tables, columns, indexes, relationships, and safe queryable data from a configured PostgreSQL database.
    11 npm
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP server for PostgreSQL that enables Cursor and other MCP clients to execute SQL queries, list tables, and explore database schemas via HTTP.
    1,240 npm
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    A self-hostable PostgreSQL MCP server for exploring database schemas and running guarded read/write queries with selectable access modes (readonly, readwrite, admin), plus a dry-run confirm workflow for safety.
    14
    1
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    MCP server for PostgreSQL, providing schema discovery, catalog introspection, read-only querying, and optional safe write operations to AI assistants.
    8
    30 npm
    PolyForm Noncommercial 1.0.0