Skip to main content
Glama
ranson21

postgres-readonly-mcp

by ranson21

postgres-readonly-mcp

A small, dependency-light MCP server that gives local coding agents (OpenCode, or anything else that speaks MCP over stdio) safe, read-only access to a PostgreSQL database — without ever handing the LLM your database credential.

This is the same pattern as jira-readonly-mcp: the MCP server owns the credential, calls the real backend itself, and hands the model back compact, sanitized results.

This project is read-only. It cannot INSERT, UPDATE, DELETE, or run any DDL/admin statement. See Threat & security model for exactly how that's enforced — including a live end-to-end check, done while building this, that a real DELETE against a real Postgres database was rejected by Postgres itself.

OpenCode / local LLM
     │  MCP (stdio, JSON-RPC)
     ▼
postgres-readonly-mcp   ← owns the DB credential; the LLM never sees it
     │  PostgreSQL wire protocol (read-only session)
     ▼
PostgreSQL

Threat & security model

Unlike a Jira API token, a Postgres credential has no built-in "this can only read" mode, and "read-only SQL" is user-supplied arbitrary text, so this server layers three independent protections rather than relying on any single one:

  1. Every session is opened read-only at the protocol level. src/db/pool.ts connects with the Postgres startup option -c default_transaction_read_only=on. This means Postgres itself refuses any write statement - INSERT/UPDATE/DELETE/DDL/etc. all fail with ERROR: cannot execute ... in a read-only transaction - regardless of what SQL text reaches it, even if this server's own validator has a bug, and even if the connected role otherwise has full write privileges. This was verified live against a real Postgres 16 instance while building this project: connecting with this exact startup option and a full-privilege role, a DELETE was rejected by Postgres with that exact error; the same DELETE without the option succeeded. This is the real enforcement boundary.

  2. Queries are always sent via the extended query protocol (pool.query({ text, values }), never a bare string), which restricts a single Parse message to exactly one SQL statement. A stacked SELECT 1; DROP TABLE x payload is rejected structurally before Postgres even tries to run the second statement.

  3. You should connect as a Postgres role that only has SELECT grants. This server can't create that role for you, but it's the credential-level backstop in case the two protections above are ever bypassed (e.g. by connecting to this same database directly, outside this server, with the same credential). See Recommended: a dedicated read-only role.

On top of those three, src/security/guard.ts adds a fail-closed, defense-in-depth static check on every run_query call, before anything is sent to Postgres: it rejects any query that doesn't start with SELECT/WITH, contains a second statement, contains an INSERT/UPDATE/DELETE/DDL/session-control keyword anywhere (which also catches the classic "data-modifying CTE" bypass, e.g. WITH x AS (DELETE FROM t RETURNING *) SELECT * FROM x), contains a row-locking clause (FOR UPDATE/FOR SHARE), or calls one of a documented list of functions (nextval, setval, pg_advisory_lock, dblink_exec, etc.) that Postgres specifically exempts from read-only transactions. This validator is a heuristic, not a SQL parser - it can reject a legitimate query whose text merely contains a blocked keyword or a semicolon inside a string literal (fails closed, on purpose), and it is not a substitute for layers 1-3 above.

Output is also passed through src/security/sanitize.ts, which redacts common credential shapes (connection strings, Bearer/Basic auth headers, password=/api_key=-style assignments, PEM private key blocks) that might otherwise be echoed back in a verbose driver error message. This is defense-in-depth, not a security boundary - it's a best-effort regex scrubber, not a guarantee, and it does nothing to protect actual row data (that's the point of the tool - returning your own data to your own agent).

What this does not protect against: if the credential you give this server can write, and somehow bypasses both the read-only session setting and the extended-protocol restriction (not a known attack against a stock pg/Postgres setup, but assume nothing is bulletproof), it could write. The role-scoping recommendation below is what protects against that residual case. Anyone who can run this process or read your .env can read anything that credential can read.

CREATE ROLE mcp_readonly WITH LOGIN PASSWORD 'choose-a-real-password';
GRANT CONNECT ON DATABASE your_database TO mcp_readonly;
GRANT USAGE ON SCHEMA public TO mcp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;
-- Run this too, so future tables are covered automatically:
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_readonly;

Point PGUSER/PGPASSWORD (or DATABASE_URL) at this role, not an application or admin account. With multiple environments, create one such role per database and point each DB_ENV_<NAME>_* block at its own — don't reuse one role's credential across environments.

Related MCP server: PostgreSQL MCP Server

Available tools

Every tool below (except list_environments) takes an optional env argument selecting which configured database to use — see Multiple environments. In single-database mode (the default), env never needs to be passed.

Tool

Description

list_environments()

Lists configured environment names and non-secret connection info (never credentials). Use a returned name as env elsewhere.

run_query(sql, env?, limit?)

Runs a single read-only SELECT/WITH ... SELECT query. Rejects anything else before it reaches Postgres. Results are capped by PG_MAX_ROW_LIMIT.

list_schemas(env?, include_system?)

Lists schemas (excludes pg_catalog/information_schema/etc. by default).

list_tables(env?, schema?)

Lists tables and views in a schema (default public).

describe_table(table, env?, schema?)

Column names, types, nullability, defaults, and primary key, for a table.

describe_table's primary-key detection deliberately queries pg_catalog (pg_index/ pg_class/pg_attribute) rather than information_schema.key_column_usage: that information_schema view only shows a constraint's columns to a role with more than SELECT on the table, so it silently reports no primary key at all for exactly the kind of SELECT-only role this README recommends. This was caught by testing against a real read-only role while building this project, not assumed.

Multiple environments

By default this server connects to one database (configured with DATABASE_URL or PGHOST/etc., as below). If you work across several databases — dev/staging/prod, or several unrelated projects — configure them all at once and select by name per tool call instead of running separate server instances or editing .env to switch:

DB_ENVIRONMENTS=dev,staging,prod
DB_DEFAULT_ENV=dev              # optional; required only if you want an implicit default

DB_ENV_DEV_HOST=dev-db.example.internal
DB_ENV_DEV_DATABASE=appdb
DB_ENV_DEV_USER=readonly_user
DB_ENV_DEV_PASSWORD=...

DB_ENV_STAGING_URL=postgres://readonly_user:...@staging-db.example.internal:5432/appdb

DB_ENV_PROD_HOST=prod-db.example.internal
DB_ENV_PROD_DATABASE=appdb
DB_ENV_PROD_USER=readonly_user
DB_ENV_PROD_PASSWORD=...
DB_ENV_PROD_SSLMODE=verify-full

Each environment can use either a DB_ENV_<NAME>_URL connection string or discrete DB_ENV_<NAME>_HOST/_PORT/_DATABASE/_USER/_PASSWORD fields, exactly like the single-database vars — just prefixed with DB_ENV_<NAME>_. _SSLMODE is also per-environment (falls back to the global PGSSLMODE if unset), since it's common for dev to run unencrypted on localhost while prod requires verify-full. Row limits, statement timeout, and cell-length truncation are global settings shared by every environment.

Environment names: letters/digits/_/-, starting with a letter (e.g. dev, qa-east, prod). A name with a hyphen maps to an underscored var prefix — qa-east reads DB_ENV_QA_EAST_*.

Once configured, call list_environments to see what's available, then pass env to any other tool: run_query({ env: "prod", sql: "SELECT count(*) FROM orders" }). If you configure more than one environment and don't set DB_DEFAULT_ENV, omitting env returns a clear error listing the valid names — it never silently guesses which database you meant.

Each named environment gets its own connection pool and its own recommended SELECT-only role (see below) — treat each one's credential with the sensitivity of that environment (a prod credential deserves more caution than a dev one, even though this server enforces read-only identically for both).

Installation on macOS

Requires Node.js 18.17+ (Apple Silicon or Intel; no native/compiled dependencies - pg's JS driver, no libpq required).

git clone https://github.com/ranson21/postgres-readonly-mcp.git
cd postgres-readonly-mcp
npm install
npm run build

This produces dist/index.js, a plain Node script you point OpenCode (or any MCP client) at.

Authentication configuration

Copy the example env file and fill in your real values — do not commit .env (it's already git-ignored):

cp .env.example .env

.env.example

# Copy this file to .env and fill in real values.
# .env is git-ignored - never commit it.
#
# All values below are FAKE placeholders for illustration only.

# ============================================================================
# SINGLE DATABASE (default) - use this section if you only have one database.
# Leave DB_ENVIRONMENTS (below) unset.
# ============================================================================

# Option A: a single connection string (takes precedence if set).
# DATABASE_URL=postgres://readonly_user:REPLACE_WITH_YOUR_PASSWORD@db.example.internal:5432/appdb

# Option B: discrete fields, used only if DATABASE_URL is not set.
PGHOST=db.example.internal
PGPORT=5432
PGDATABASE=appdb
PGUSER=readonly_user
PGPASSWORD=REPLACE_WITH_YOUR_PASSWORD

# TLS mode: disable | require | verify-full
# - disable: no encryption (fine for localhost/dev only)
# - require: encrypted, but does NOT verify the server certificate
# - verify-full: encrypted AND verifies the server certificate (recommended
#   for anything that isn't localhost)
# PGSSLMODE=disable

# ============================================================================
# MULTIPLE NAMED DATABASES - set DB_ENVIRONMENTS to a comma-separated list of
# names, then give each one its own DB_ENV_<NAME>_* vars below. When set,
# this takes over entirely from the single-database vars above. Tools take
# an `env` argument to pick which one to use. See "Multiple environments".
# ============================================================================

# DB_ENVIRONMENTS=dev,staging,prod

# --- dev ---
# DB_ENV_DEV_URL=postgres://readonly_user:REPLACE_WITH_YOUR_PASSWORD@dev-db.example.internal:5432/appdb
# or discrete fields instead of DB_ENV_DEV_URL:
# DB_ENV_DEV_HOST=dev-db.example.internal
# DB_ENV_DEV_PORT=5432
# DB_ENV_DEV_DATABASE=appdb
# DB_ENV_DEV_USER=readonly_user
# DB_ENV_DEV_PASSWORD=REPLACE_WITH_YOUR_PASSWORD
# DB_ENV_DEV_SSLMODE=disable

# --- staging ---
# DB_ENV_STAGING_HOST=staging-db.example.internal
# DB_ENV_STAGING_PORT=5432
# DB_ENV_STAGING_DATABASE=appdb
# DB_ENV_STAGING_USER=readonly_user
# DB_ENV_STAGING_PASSWORD=REPLACE_WITH_YOUR_PASSWORD
# DB_ENV_STAGING_SSLMODE=require

# --- prod ---
# DB_ENV_PROD_HOST=prod-db.example.internal
# DB_ENV_PROD_PORT=5432
# DB_ENV_PROD_DATABASE=appdb
# DB_ENV_PROD_USER=readonly_user
# DB_ENV_PROD_PASSWORD=REPLACE_WITH_YOUR_PASSWORD
# DB_ENV_PROD_SSLMODE=verify-full

# Which environment a tool call uses when it doesn't pass `env` explicitly.
# Required if you configure more than one environment and want an implicit
# default; otherwise every tool call must pass `env`. Not needed at all in
# single-database mode (DB_ENVIRONMENTS unset).
# DB_DEFAULT_ENV=dev

# --- Read-only enforcement (applies to every environment) ---
# This server always opens sessions with default_transaction_read_only=on,
# so Postgres itself rejects write statements regardless of what SQL text
# reaches it. On top of that, STRONGLY prefer connecting as a Postgres role
# that only has SELECT grants, for every environment - see README
# "Threat & security model".

# --- Optional tuning (applies to every environment) ---
# Default/maximum rows returned by run_query (an outer LIMIT is always
# applied on top of whatever the query itself requests).
# PG_DEFAULT_ROW_LIMIT=100
# PG_MAX_ROW_LIMIT=500

# Server-side statement timeout, in milliseconds. Protects against a runaway
# query; enforced by Postgres itself via the session's statement_timeout.
# PG_STATEMENT_TIMEOUT_MS=15000

# Long text/jsonb cell values are truncated to this many characters before
# being returned to the model, to avoid wasting context on huge blobs.
# PG_MAX_CELL_LENGTH=2000

# Client-side connection timeout, in milliseconds.
# PG_CONNECT_TIMEOUT_MS=10000

# Optional: diagnostic log verbosity. One of: debug | info | warn | error.
# Logs always go to stderr, never stdout (stdout is reserved for MCP JSON-RPC
# traffic). Defaults to "info".
# DB_MCP_LOG_LEVEL=info

Two ways to configure a single database's connection:

  • DATABASE_URL (takes precedence if set): a single postgres://user:password@host:port/db connection string.

  • Discrete fields: PGHOST, PGPORT (default 5432), PGDATABASE, PGUSER, PGPASSWORD.

To connect to several databases instead (dev/staging/prod, or several unrelated projects), see Multiple environments — same two options, just per-environment.

Either way, set PGSSLMODE appropriately for where the database lives:

  • disable (default) - fine for localhost/a private network you trust.

  • require - encrypts the connection but does not verify the server certificate.

  • verify-full - encrypts and verifies the certificate. Use this for anything that isn't localhost, especially a managed/cloud Postgres instance.

No database administrator action is required beyond creating the recommended read-only role (a normal CREATE ROLE/GRANT any role with CREATEROLE, or a DBA, can run) — this server only ever uses ordinary SQL a authenticated role can run.

Optional: macOS Keychain

Keep the password out of a plaintext .env file:

security add-generic-password -a "$USER" -s postgres-readonly-mcp-password -w 'your-password-here'

Then wrap the launch command so PGPASSWORD is populated from Keychain right before the server starts:

#!/bin/sh
# run.sh
export PGPASSWORD="$(security find-generic-password -a "$USER" -s postgres-readonly-mcp-password -w)"
exec node "$(dirname "$0")/dist/index.js"

Point OpenCode's command at run.sh instead of node dist/index.js directly, and drop PGPASSWORD from the environment block.

OpenCode MCP configuration

Add this to your OpenCode config (e.g. opencode.json or ~/.config/opencode/opencode.json), using OpenCode's local MCP server schema:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "postgres": {
      "type": "local",
      "command": ["node", "/absolute/path/to/postgres-readonly-mcp/dist/index.js"],
      "enabled": true,
      "environment": {
        "PGHOST": "db.example.internal",
        "PGPORT": "5432",
        "PGDATABASE": "appdb",
        "PGUSER": "mcp_readonly",
        "PGPASSWORD": "your-real-password-goes-here-not-in-git"
      }
    }
  }
}

Use an absolute path for command — OpenCode resolves it relative to its own working directory, not this repo. If you're using the Keychain wrapper above, point command at run.sh and drop PGPASSWORD from environment.

For multiple environments, put the DB_ENVIRONMENTS/DB_ENV_<NAME>_* vars in environment instead — one server instance then serves all of them, selected per tool call by name:

{
  "environment": {
    "DB_ENVIRONMENTS": "dev,staging,prod",
    "DB_DEFAULT_ENV": "dev",
    "DB_ENV_DEV_HOST": "dev-db.example.internal",
    "DB_ENV_DEV_DATABASE": "appdb",
    "DB_ENV_DEV_USER": "mcp_readonly",
    "DB_ENV_DEV_PASSWORD": "...",
    "DB_ENV_STAGING_HOST": "staging-db.example.internal",
    "DB_ENV_STAGING_DATABASE": "appdb",
    "DB_ENV_STAGING_USER": "mcp_readonly",
    "DB_ENV_STAGING_PASSWORD": "...",
    "DB_ENV_PROD_HOST": "prod-db.example.internal",
    "DB_ENV_PROD_DATABASE": "appdb",
    "DB_ENV_PROD_USER": "mcp_readonly",
    "DB_ENV_PROD_PASSWORD": "...",
    "DB_ENV_PROD_SSLMODE": "verify-full"
  }
}

Example tool calls

  • "List the tables in the database." (list_tables)

  • "Describe the orders table." (describe_table)

  • "Run a query to find the 10 most recent orders with status 'open'." (run_query with SELECT * FROM orders WHERE status = 'open' ORDER BY created_at DESC LIMIT 10)

  • "What schemas exist in this database?" (list_schemas)

  • With multiple environments configured: "What database environments are available?" (list_environments), then "In the prod environment, how many orders were placed today?" (run_query with env: "prod")

A run_query result looks like:

{
  "rowCount": 2,
  "columns": ["id", "name", "price"],
  "rows": [
    { "id": 1, "name": "Fake Widget A", "price": "9.99" },
    { "id": 2, "name": "Fake Widget B", "price": "19.99" }
  ],
  "truncated": false
}

truncated: true means the row count hit the applied limit — there may be more rows than shown. Long text/jsonb cell values are truncated (see PG_MAX_CELL_LENGTH); binary (bytea) values are replaced with a [binary N bytes omitted] placeholder, never returned raw.

Logging

Diagnostic logs go to stderr only. This server never writes to stdout, since stdout is the MCP JSON-RPC transport for this stdio server — any stray line there would corrupt the protocol stream. console.error is used everywhere logging happens; console.log is never used.

Control verbosity with DB_MCP_LOG_LEVEL (debug | info | warn | error, default info):

[2026-09-13T01:40:45.900Z] [INFO] [postgres-readonly-mcp] tool invoked {"tool":"run_query","env":"prod","limit":10}
[2026-09-13T01:40:46.031Z] [INFO] [postgres-readonly-mcp] query complete {"env":"prod","elapsedMs":12,"rowCount":3}
[2026-09-13T01:40:46.032Z] [INFO] [postgres-readonly-mcp] tool succeeded {"tool":"run_query"}

Every log line produced while handling a given environment's queries carries "env":"<name>", so with multiple environments configured you can tell at a glance (or with grep) which database a given query hit — useful when dev and prod are both configured and something in prod needs attention.

Event

Level

Server startup (each configured environment's host/port/database, TLS mode, row limits, and the default env — never a password)

info

Tool invoked (tool name, resolved env name, table/schema/limit as applicable)

info

The raw SQL text passed to run_query

debug only

Query complete (elapsed ms, row count)

info

Sanitization complete (tool name only)

debug

Tool succeeded (tool name only)

info

Errors (auth failures, connection failures, rejected queries, query errors)

error

The raw SQL text is deliberately logged at debug, not info: unlike a Jira issue key, a WHERE clause can carry literal row data (emails, IDs, etc.) that shouldn't land in default logs. Turn on DB_MCP_LOG_LEVEL=debug when you need to see exactly what was run.

Never logged, at any level: the database password, a full connection string, Authorization headers, or raw driver error internals beyond a redacted message. As an extra safety net, every structured log field also passes through the same redaction layer used for tool output before being written — see Threat & security model for why that's defense-in-depth, not a guarantee.

Verifying the server without exposing credentials

npm test    # 100 tests, all against fixtures/fakes - no network, no real database required
npm run build

To check the live server without ever printing a real password to a terminal a shared log might capture, use the MCP Inspector:

PGPASSWORD="$(security find-generic-password -a "$USER" -s postgres-readonly-mcp-password -w)" \
npx @modelcontextprotocol/inspector node dist/index.js

Inspector lets you call tools/list and tools/call interactively in a browser UI. Try run_query with an obvious mutation (e.g. DELETE FROM some_table) and confirm it's rejected with "Only SELECT (or WITH ... SELECT) queries are allowed" before it ever reaches the database.

Troubleshooting

  • "Missing database connection settings" — you haven't set DATABASE_URL, or all of PGHOST/PGDATABASE/PGUSER/PGPASSWORD. See Authentication configuration.

  • DbAuthError (password authentication failed) — check PGUSER/PGPASSWORD, and that the role is allowed to connect from wherever this server runs (pg_hba.conf on self-managed Postgres).

  • DbConnectionError (connection refused/timed out) — check PGHOST/PGPORT, network reachability, and PGSSLMODE (a managed Postgres provider often requires require or verify-full).

  • A query is rejected with "Only SELECT (or WITH ... SELECT) queries are allowed" even though it looks like a SELECT — the validator is a heuristic (see Threat & security model): a semicolon or a blocked keyword inside a string literal in your query text will trigger this. Rewrite to avoid the pattern.

  • describe_table shows every column as isPrimaryKey: false — this was a real bug caught while building this project when connected as a SELECT-only role; it's fixed by querying pg_catalog directly (see Available tools). If you still see it, you're likely running an older build — npm run build again.

  • Row values look truncated — increase PG_MAX_CELL_LENGTH, or note that truncated: true on the result means more rows exist beyond PG_MAX_ROW_LIMIT/the requested limit.

  • OpenCode doesn't see the tools — double check command is an absolute path to dist/index.js and that you ran npm run build.

Running entirely locally

Aside from the connection this server makes directly to your configured PGHOST (or the host in DATABASE_URL), everything else — the MCP process, the OpenCode/local-model side, tool schema handling, the SQL guard, and the sanitization layer — runs entirely on your machine. No third-party service, telemetry endpoint, or analytics call is contacted. This makes it a good fit for local model setups (OpenCode + a locally-served model, e.g. via MLX/OptiQ) where you want to keep everything except the necessary database traffic on-device.

Testing

npm test

100 tests, all against fixtures/fakes/a fake pg-shaped pool — no real database, no network access. Coverage includes: the SQL guard (allowed queries, mutation rejection, the data-modifying-CTE bypass attempt, stacked statements, locking clauses, side-effecting functions, documented false-positive limitations), row/value normalization (dates, bigints, buffers, truncation), config parsing (DATABASE_URL vs discrete fields, SSL mode mapping), credential redaction (by pattern and by key name), error classification (auth vs. connection vs. query errors), and schema-introspection query shape (parameterized, never string-interpolated).

On top of the unit tests, this project's read-only guarantees were verified live against a real local Postgres 16 instance (see Threat & security model) — a DELETE was attempted and rejected by Postgres itself under the exact startup option this server uses, both as the recommended SELECT-only role and as a full-privilege role.

Development

npm run dev    # run src/index.ts directly with tsx, for local iteration
npm run build  # compile to dist/
npm test       # run the test suite

License

MIT — see LICENSE.

Available Tools

5 tools
describe_tableA

Describe a table's columns (name, type, nullable, default, primary key) in a schema (default: public). Read-only.

ParametersJSON Schema
NameRequiredDescriptionDefault
envNoWhich configured database environment to use. Only "default" is configured, so this can be omitted.
tableYesTable name
schemaNoSchema name, defaults to "public"

TDQS

A3.6/5.0
Behavior3/5

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

No annotations are provided, so the description must carry the behavioral burden. It does declare 'Read-only', which is valuable safety context, but omits what happens on an unknown table/schema (error vs empty) and whether it touches metadata only. Some value added, but thin for a zero-annotation tool.

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: verb, resource, returned fields, then the default and safety note appended. No filler, every clause earns its place.

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

Completeness4/5

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

There is no output schema, but the description compensates by enumerating the returned columns, so an agent knows what to expect. Minor gap: no indication of error behavior or whether schema/table are case-sensitive, but the essentials are covered.

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 all three parameters are already documented in the schema, including the 'public' default. The description's '(default: public)' simply restates the schema's own note and adds no new semantics. Baseline 3.

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 ('Describe') and resource ('a table's columns') and enumerates exactly what is returned (name, type, nullable, default, primary key). It does not name or distinguish itself against siblings like list_tables or run_query, which would be needed for 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 only implied: an agent can infer this is the introspection tool used before querying, but the description never says when to prefer it over list_tables or run_query, nor any prerequisites. Minimum viable guidance, no explicit when-to-use or exclusions.

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

list_environmentsA

List configured database environments (name and non-secret connection info, never credentials). Use the returned names as the env argument to other tools. Read-only.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.5/5.0
Behavior4/5

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

With no annotations, the description carries the full burden and does disclose the two traits that matter here: 'Read-only' and 'never credentials,' which reassures the agent that invoking it is safe and that secrets are not exposed. It does not mention auth requirements or whether the environment list can change at runtime, but for a simple enumeration tool this is solid 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?

Three short sentences, all front-loaded and each earning its place: what it lists, what it returns, and how to use the result. No filler or redundancy.

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?

No output schema exists, so the description must describe the return shape, and it does ('name and non-secret connection info, never credentials'). Combined with the read-only disclosure and the env-routing hint, an agent has everything needed to call this correctly.

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

Parameters4/5

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

The tool takes zero parameters, so there is nothing for the description to disambiguate and the baseline is 4. It usefully clarifies that the output feeds the `env` parameter of sibling tools, which is the only argument-related information relevant here.

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 ('List configured database environments') and immediately scopes what is returned (name and non-secret connection info). This clearly distinguishes it from siblings like list_schemas and list_tables, which operate inside an environment rather than enumerating environments themselves.

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?

Explicitly tells the agent how to use the output: 'Use the returned names as the `env` argument to other tools,' which is the key routing guidance for this discovery tool. It lacks an explicit when-not-to-use or a named alternative, but for a zero-param discovery call the contextual guidance is strong.

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

list_schemasA

List schemas in the database. Excludes system schemas by default. Read-only.

ParametersJSON Schema
NameRequiredDescriptionDefault
envNoWhich configured database environment to use. Only "default" is configured, so this can be omitted.
include_systemNoInclude pg_catalog/information_schema/pg_temp_*/pg_toast* schemas

TDQS

A3.6/5.0
Behavior3/5

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

With no annotations, the description carries the full burden; it does disclose read-only status and the default exclusion of system schemas, which is genuinely useful. However, it omits anything about permissions, result size/pagination, or ordering of the returned schemas.

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 core action and free of filler. Every clause adds information (scope, default behavior, safety).

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

Completeness4/5

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

For a simple zero-required-parameter listing tool with full schema coverage and no output schema, the description covers what it does, its default filtering, and its safety profile. A brief note on the returned fields would make it 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 coverage is 100%, so both parameters are already documented by the schema; baseline is 3. The description's 'excludes system schemas by default' reinforces the include_system parameter, but adds no syntax or format detail beyond the schema text.

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 ('List schemas in the database') in the first sentence, which clearly separates it from siblings like list_tables and list_environments. It does not explicitly name alternatives, but the resource distinction is unambiguous.

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?

Gives a default-behavior cue ('Excludes system schemas by default'), which implies the common case, but never says when to choose this over list_tables or describe_table, nor any prerequisites. Usage is implied rather than stated.

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 and views in a schema (default: public). Read-only.

ParametersJSON Schema
NameRequiredDescriptionDefault
envNoWhich configured database environment to use. Only "default" is configured, so this can be omitted.
schemaNoSchema name, defaults to "public"

TDQS

A3.6/5.0
Behavior3/5

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

With no annotations, the description carries the full behavioral burden, and it does disclose the key trait: 'Read-only.' That is genuinely useful safety context. However, it says nothing about pagination, result limits, ordering, or permissions, so it is only partially complete for a no-annotation tool.

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

Conciseness5/5

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

A single compact sentence, front-loaded with the action and resource, followed by the default and the read-only note. Every clause earns its place with no filler.

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

Completeness4/5

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

For a simple two-parameter read tool with no output schema, the description covers what is listed, the default schema, and the safety profile. It is slightly thin on what the listing returns (names only? with types?), but no critical calling information 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 both parameters (env, schema) are already documented, including the 'public' default and the enum note. The description merely restates the schema default, adding no syntax or format detail beyond the schema. 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 (list tables and views) plus the scope (a schema, defaulting to public), which cleanly separates it from list_schemas and describe_table. It does not explicitly name or contrast with any sibling, so it falls just 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 name and resource: reach for it to discover what tables/views exist before describing or querying them. There is no explicit when-to-use guidance, no mention of when to prefer list_schemas or describe_table, and no stated prerequisites.

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

run_queryA

Run a read-only SQL query (SELECT, or WITH ... SELECT) against the database and return compact rows. Results are capped by PG_MAX_ROW_LIMIT. Any INSERT/UPDATE/DELETE/DDL/etc. is rejected before it reaches the database - this server is read-only.

ParametersJSON Schema
NameRequiredDescriptionDefault
envNoWhich configured database environment to use. Only "default" is configured, so this can be omitted.
sqlYesA single SELECT (or WITH ... SELECT) statement, e.g. "SELECT * FROM orders WHERE status = 'open'"
limitNoMax rows to return

TDQS

A3.8/5.0
Behavior4/5

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

With no annotations, the description carries the full burden and does well: it discloses the read-only server guarantee, that non-SELECT statements are rejected before reaching the database, and that output is capped by PG_MAX_ROW_LIMIT and returned as compact rows. It omits auth requirements and what happens when the cap is hit (error vs truncation), which keeps it short of 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?

Three tight sentences with the core capability front-loaded and the read-only constraint immediately after. Every clause carries information; there is no filler.

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 single-query tool with no output schema, the description covers the allowed statement forms, the write-rejection behavior, and the row-cap/return format. It stops just short of describing failure behavior at the cap or transaction semantics, which would make it fully self-contained.

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. The description adds some value by tying the row ceiling to PG_MAX_ROW_LIMIT, but it does not clarify how the user-supplied 'limit' interacts with that server cap or what the 'env' enum really means beyond the schema text.

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 (Run) and resource (read-only SQL query) with the exact statement shapes allowed (SELECT, WITH ... SELECT), so the operation is unmistakable. It does not explicitly name how it differs from the introspection siblings (list_tables, describe_table), but the execution-vs-metadata distinction is obvious from the phrasing.

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?

Gives a clear when-not: writes and DDL are rejected, so the agent knows this tool is only for reads. It never routes the agent between this and the sibling tools (e.g. use describe_table to inspect schema before querying), leaving usage context implied rather than stated.

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. 5 tool updatesv0.2.0
    • First observeddescribe_table
    • First observedlist_environments
    • First observedlist_schemas
    • First observedlist_tables
    • First observedrun_query

TDQS

A4/5.0

Scored across 5 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: listing environments, running queries, listing schemas, listing tables, and describing a table. There is no overlap in their intended use, and descriptions reinforce the boundaries.

Naming Consistency5/5

All tool names follow a consistent snake_case verb_noun pattern: list_environments, run_query, list_schemas, list_tables, describe_table. The slight variation in verbs (list, run, describe) is appropriate and predictable.

Tool Count5/5

Five tools are well-scoped for a read-only PostgreSQL server, covering environment discovery, query execution, and schema introspection without bloat or omission of core operations.

Completeness4/5

The surface covers essential read-only operations: environment listing, ad-hoc queries, schema/table listing, and column description. Minor gaps exist for more granular introspection (e.g., indexes, foreign keys, constraints), but agents can work around them with run_query.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables LLMs to query and analyze PostgreSQL databases through a controlled interface. Supports SQL query execution, table schema inspection, and optional write operations with safety controls.
    84 npm
    180
    MIT
  • A
    license
    Not graded
    quality
    Not graded
    maintenance
    Provides AI assistants with safe, controlled access to PostgreSQL databases with read-only defaults, granular permissions, query safety features, and schema introspection capabilities.
    1
    -
  • F
    license
    A
    quality
    D
    maintenance
    Enables AI agents to inspect and query PostgreSQL databases safely, with features like listing tables, retrieving schemas, and running read-only SQL queries.
    3
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI assistants to interact with PostgreSQL databases, providing tools for querying, table listing, schema exploration, and multi-environment management with write protection and schema scoping.
    1
    MIT