Skip to main content
Glama

pg-mcp

English | Русский

tests release downloads license: MIT

An MCP server that gives an agent read-only access to a PostgreSQL database, or through one entry to dozens of stands. The agent sees the database structure, can look at sample rows and query plans, and analysts can run their own SELECTs. Nothing can change data, structure or settings through this server.

Built for GigaCode CLI and Qwen Code, and works with any MCP client that starts servers over stdio. Runs on Python 3.9+ with nothing to install: the pg8000 driver ships inside the repository. Speaks English or Russian.

Contents

Related MCP server: PostgreSQL MCP Server

What it does

Tool

What it does

Profiles

status

which database, as which role, which profile and language, which limits; on failure, what to fix

all

list_schemas

with many stands: the databases of a stand and their schemas the server has logins for

all

list_tables

tables, views and foreign tables the role can read, with estimated row counts and comments

all

search_schema

tables, views and columns whose name or comment contains a piece of text, in every readable schema; for large databases

all

describe_table

columns with types, NOT NULL, defaults and comments; keys, tables referencing it, indexes

all

compare_schema

with many stands: how one schema differs between two stands: tables and views, columns, constraints, indexes

all

sample_rows

a few rows of a table, optionally only some columns

explore, analyst

explain_query

a query plan without running the query; in the analyst profile also with real timings (ANALYZE)

explore, analyst

run_select

one read-only statement: SELECT, WITH … SELECT, VALUES, TABLE, EXPLAIN, SHOW

analyst

The group's administrator picks the profile at installation: details.

Quick start

You need Python 3.9+, GigaCode CLI (or Qwen Code) and a database role from your DBA.

git clone https://github.com/dbelokursky/pg-mcp.git
cd pg-mcp
export PGHOST=db.example.com PGDATABASE=app PGUSER=mcp_agent   # where the database is, which role
python3 install.py --lang en                                     # register and check
gigacode extensions link "$(pwd)"                                # context and the /pg:setup command

Put the password into ~/.pgpass (how), restart GigaCode and ask the agent: "Check the database connection".

There is a shorter way too: link the extension (gigacode extensions link), restart GigaCode and run /pg:setup in it. The agent finds Python, runs install.py and works through the hints itself.

Try it on the demo database

The answers shown in Usage come from a small demo database: a shop the agent may read and an hr schema it may not. scripts/demo.py makes it on any PostgreSQL where your PG* variables may create a database and a role (a fresh local PostgreSQL will do):

python3 scripts/demo.py                                     # pg_mcp_demo and the role mcp_demo_agent, password demo
echo "localhost:5432:pg_mcp_demo:mcp_demo_agent:demo" >> ~/.pgpass && chmod 600 ~/.pgpass
PGDATABASE=pg_mcp_demo PGUSER=mcp_demo_agent python3 install.py --profile analyst

Restart the agent and ask it: "How many orders are there per status?", "Describe the orders table", "What is in the hr schema?". The data is the same on every copy (docs/demo.sql builds it with arithmetic, not random()), so the agent's answers match the examples in this README. python3 scripts/demo.py --drop removes the database and the role.

Installation step by step

1. The code

git clone https://github.com/dbelokursky/pg-mcp.git

Without git, take pg-mcp-<version>.zip from the latest release on the Releases page and unpack it anywhere: inside is a pg-mcp folder with everything the server needs, and SHA256SUMS next to the archive holds its checksum. The archive carries a provenance attestation: gh attestation verify pg-mcp-<version>.zip --repo dbelokursky/pg-mcp confirms that GitHub built it from this repository's tag. Do not move the folder afterwards: its path goes into the agent settings. If you do move it, repeat step 5.

2. Python 3.9 or newer

System

How to check

Where to get it

macOS

python3 --version

comes with the Command Line Tools (installing git brings them)

Linux

python3 --version

your distribution's python3 package

Windows

py -3 --version or python --version

the installer from python.org, or the "embeddable package" zip, which unpacks without admin rights

Nothing needs to be installed with pip.

3. Where the database is

The server reads the same environment variables as psql. Set them in the terminal you run install.py from, and the script copies them into the server settings.

macOS and Linux:

export PGHOST=db.example.com PGPORT=5432 PGDATABASE=app PGUSER=mcp_agent

Windows (PowerShell):

$env:PGHOST = "db.example.com"; $env:PGDATABASE = "app"; $env:PGUSER = "mcp_agent"

For TLS with certificate checks add PGSSLMODE=verify-full and PGSSLROOTCERT=<path to the CA>. All variables are listed under settings.

4. The password goes into the password file

The password lives only in PostgreSQL's password file, the same one psql reads. Never put it into the agent settings and never send it in the chat.

macOS and Linux use ~/.pgpass:

nano ~/.pgpass        # or any other editor
chmod 600 ~/.pgpass   # otherwise the file is ignored: others could read it

Windows uses %APPDATA%\postgresql\pgpass.conf:

mkdir "$env:APPDATA\postgresql" -Force
notepad "$env:APPDATA\postgresql\pgpass.conf"

One line per connection: host:port:database:role:password. An asterisk matches any value; escape a colon or a backslash inside a value as \: and \\.

db.example.com:5432:app:mcp_agent:password-from-the-DBA

5. Register the server with the agent

python3 install.py                              # GigaCode, profile explore, language from the system locale
python3 install.py --profile analyst --lang en  # profile and language spelled out
python3 install.py --client qwen                # Qwen Code instead of GigaCode
py -3 install.py                                # Windows

What the script does:

  1. backs up the agent settings (~/.gigacode/settings.json.bak-<date>);

  2. adds the server pg to mcpServers: the Python that runs the script, the path to run.py, the PG* variables from the terminal (except PGPASSWORD), the profile and the language;

  3. starts the server the way the agent will, calls status and prints the answer. Exit code 0 means the database is reachable.

python3 install.py --check only starts the server and calls status; it changes nothing.

This is the entry in ~/.gigacode/settings.json after install.py --profile analyst --lang en:

{
  "mcpServers": {
    "pg": {
      "command": "/usr/bin/python3",
      "args": ["-I", "/Users/me/pg-mcp/run.py"],
      "timeout": 30000,
      "env": {
        "PGHOST": "db.example.com",
        "PGDATABASE": "app",
        "PGUSER": "mcp_agent",
        "PG_MCP_PROFILE": "analyst",
        "PG_MCP_LANG": "en"
      }
    }
  }
}

If settings.json contains comments, the script leaves it alone and prints the snippet to add by hand.

Several databases

One clone serves any number of databases: register each further one under its own name. Its entry becomes pg-<name> next to pg, with its own connection, profile, language and query log:

PGDATABASE=sales python3 install.py --name sales
PGDATABASE=hr PGUSER=mcp_hr python3 install.py --name hr --profile schema

The agent then gets the tools of every entry, and the status of each names its server and database.

Other agents and editors

--client registers the server with other MCP clients as well, in each one's own file and format:

--client

Where the entry goes

gigacode (the default), qwen

~/.gigacode/settings.json, ~/.qwen/settings.json

vscode

the user mcp.json of VS Code (GitHub Copilot agent mode), under servers

cursor

~/.cursor/mcp.json

windsurf

Windsurf, now Devin Desktop: ~/.config/devin/mcp_config.json, or the older file where one is in use

cline

~/.cline/data/settings/cline_mcp_settings.json, shared by Cline in VS Code, JetBrains and its CLI

roo

the settings file of the Roo Code extension in VS Code (the extension is no longer developed)

continue

a block file ~/.continue/mcpServers/pg.yaml

lmstudio

~/.lmstudio/mcp.json

claude-desktop

the app's claude_desktop_config.json

junie

~/.junie/mcp/mcp.json

jetbrains

nothing is written: AI Assistant keeps its servers in Settings, so the entry is printed to paste

python3 install.py --client cursor            # write into Cursor's file (with a backup) and check
python3 install.py --client vscode --print    # only show the entry and where it goes

--print suits a client this list lacks, or a file you would rather edit yourself. A file with comments is never rewritten: install.py prints what to add instead. Run at a terminal, install.py notices the other clients installed on this machine once it has registered the server and offers to add it there too. The extension (the context and the /pg: commands) exists for GigaCode CLI and Qwen Code only.

6. The extension

gigacode extensions link /path/to/pg-mcp     # Qwen Code: qwen extensions link …

The extension gives the agent a short context (where to start, never to ask for passwords in the chat, to answer in the user's language), the /pg:setup command for installing and repairing the server and /pg:import for bringing stand connections in from DBeaver or pg_service.conf.

7. Check

Restart GigaCode so it starts the server with the new settings, and ask: "Check the database connection". The agent calls status and tells you what it says.

Usage

Talk to the agent in plain words; it picks the tools.

You ask

The agent calls

"Check the database connection"

status

"Which tables are in the shop schema?"

list_tables

"Where are the customers stored?"

search_schema

"Describe the orders table"

describe_table

"Show me a few orders"

sample_rows

"Why is this query slow: …"

explain_query

"How many orders are there per status?"

run_select (profile analyst)

Below are real answers of the tools on a demo database. The model reads them, and you see them in the agent's interface.

status:

pg-mcp 0.7.0 on Python 3.9.6, installed at /Users/me/pg-mcp
server name: pg
access: read only. Nothing can be inserted, changed or deleted through this server: decline such requests instead of asking the user to confirm them.
target: mcp_demo_agent@localhost:5432/pg_mcp_demo (sslmode=prefer)
profile: analyst (status, list_tables, search_schema, describe_table, sample_rows, explain_query, run_select)
language: en
query log: off
database: connected to pg_mcp_demo as mcp_demo_agent, PostgreSQL 16.15
limits: read only = on, statement timeout = 5s, list_tables returns at most 200 entries, answers are cut at 8000 characters

list_tables: table comments help the model see what lives where.

shop.customers | table | ~50 rows | Покупатели
shop.order_items | table | ~2.2k rows | Позиции заказов: цена фиксируется на момент заказа
shop.orders | table | ~500 rows | Заказы
shop.products | table | ~20 rows | Каталог товаров

search_schema with text: "customer" finds the table and a column of another table:

shop.customers | table | ~50 rows | Покупатели
shop.orders.customer_id integer NOT NULL

describe_table for shop.orders:

shop.orders | table | ~500 rows | Заказы
columns:
  id integer NOT NULL
  customer_id integer NOT NULL
  status text NOT NULL  -- new, paid, shipped или cancelled
  created_at timestamp with time zone NOT NULL
constraints:
  orders_pkey: PRIMARY KEY (id)
  orders_customer_id_fkey: FOREIGN KEY (customer_id) REFERENCES shop.customers(id)
  orders_status_check: CHECK (status = ANY (ARRAY['new'::text, 'paid'::text, 'shipped'::text, 'cancelled'::text]))
referenced by:
  shop.order_items: FOREIGN KEY (order_id) REFERENCES shop.orders(id)
indexes:
  CREATE INDEX orders_created_idx ON shop.orders USING btree (created_at)
  CREATE INDEX orders_customer_idx ON shop.orders USING btree (customer_id)
  CREATE UNIQUE INDEX orders_pkey ON shop.orders USING btree (id)

The comments come from the database itself, so they stay in whatever language they were written in.

sample_rows with columns: "id, status, created_at" and limit: 3:

shop.orders:
id | status | created_at
1 | shipped | 2026-01-01 09:00:00+02:00
2 | paid | 2026-01-01 16:00:00+02:00
3 | cancelled | 2026-01-01 23:00:00+02:00
(3 rows shown, there are more: narrow the query with WHERE, ORDER BY or LIMIT)

An error is a hint as well: the model sees what to fix and tries again.

No column 'total' in shop.orders. Columns: id, customer_id, status, created_at.
DELETE is not allowed. Only read-only statements are: SELECT, WITH ... SELECT, VALUES, TABLE, EXPLAIN and SHOW.
No readable table or view named 'salaries' in schema 'hr'. Call list_tables to see what exists.

The last line is about the hr schema, which the role has no rights to: the tools show none of its tables.

Limits

What

How much

Entries from list_tables

at most 200; pass schema to narrow the list

Matches from search_schema

at most 100

Rows from sample_rows

10 by default, at most 50

Rows from run_select

at most 200

Plan lines

at most 500

One value in an answer

at most 200 characters

A whole tool answer

8000 characters (PG_MCP_MAX_RESPONSE_CHARS)

One query

5 seconds (PG_MCP_STATEMENT_TIMEOUT_MS)

Open connections of an entry for stands

at most 8 (PG_MCP_MAX_CONNECTIONS)

Stands in one call (stands)

at most 10; the rows of run_select and the matches of search_schema are shared among them, at least 10 per stand

A longer answer is cut at whole lines, and its last line says how much is shown. Local models run with small context windows, and an answer that is too long can end the agent's session.

Many stands

When there are dozens of stands (dev1 to dev50, ift1 to ift3), each with several databases and a login per schema, an entry per connection would cost the model thousands of characters of tool descriptions in every request. One entry serves them all instead: the tools are described once, and each call names the stand and the schema.

1. The registry. Connections are kept in the pg_service.conf format of libpq, a section per schema named stand.database.schema:

[dev7.demo.shop]
host=dev7-db.example.com

[dev7.demo.hr]
host=dev7-db.example.com

[ift2.demo.shop]
host=ift2-db.example.com
port=6432
user=shop_reader
  • only host is required; port (5432), dbname (the database part of the name), user (the schema name), sslmode, sslrootcert and connect_timeout (10 seconds) are optional;

  • passwords go into the password file, a line per login, just as for one database;

  • the server reads ~/.pg-mcp/stands.conf (on Windows %APPDATA%\pg-mcp\stands.conf, or the file PG_MCP_STANDS_FILE names) and your own ~/.pg_service.conf; with equal names the first file wins. Sections named otherwise are ordinary psql services and stay out of it;

  • psql reads the same sections: psql service=dev7.demo.shop.

2. The entry.

PG_MCP_STANDS='*' python3 install.py --name stands

This registers pg-stands next to pg. PG_MCP_STANDS switches stands on: * takes every stand, patterns such as dev*,ift2 take some. The check succeeds once the stands are found: a stand behind a VPN is reached only when a tool needs it.

Importing connections. When the connections already live in DBeaver, or in a pg_service.conf file, let install.py bring them in:

python3 install.py import dbeaver            # a preview: what would go where; nothing is written
python3 install.py import dbeaver --write    # write the registry and the password file, register pg-stands
python3 install.py import pg-service ~/.pg_service.conf --write

The stand comes from the DBeaver folder, then the connection name, then the host, matched with the regular expression (dev|ift)\d+ (--stand-pattern changes it); the database is the connection's and the schema is its login, since on a stand every schema has a login of its own name. The preview lists what would be written and what is left out and why; passwords are never shown. --write rewrites ~/.pg-mcp/stands.conf (adding to what is there, or replacing it with --prune), adds the passwords to the password file without touching its other lines, keeps a dated backup of both files and registers pg-stands; when anything fails, nothing is changed. DBeaver's saved passwords are read from its own files while DBeaver keeps them under its default key; with its secure storage switched on they stay unknown, and the preview says which logins need a line in the password file by hand. The agent does the same with /pg:import, writing only after you confirm. A running server notices a changed registry on its next call, so an import needs no restart; a registry file that holds a password= line and is readable by other users gets a warning in status.

Exporting connections to database tools. The other way round, install.py export puts the stands into the tools you query them with yourself: a folder or group per stand, every connection logging in with the password file. No password is copied anywhere.

python3 install.py export dbeaver                # a preview: what would go where; nothing is written
python3 install.py export dbeaver --write
python3 install.py export pgadmin --write --stands 'dev*,ift2'

Tool

export

What it writes

Then

DBeaver

dbeaver

data-sources-pg-mcp.json next to DBeaver's data-sources.json, or in the folder --workspace names

Restart DBeaver

DataGrip and the JetBrains IDEs with database tools

datagrip

.idea/dataSources.xml and dataSources.local.xml in ~/.pg-mcp/datagrip, or in the project --project names (close it first)

Open the folder as a project

pgAdmin 4

pgadmin

~/.pg-mcp/pgadmin-servers.json

Tools > Import/Export Servers > Import

psql, pgcli

pg-service

Sections of ~/.pg_service.conf, the schema first on the search path

psql "service=dev7.demo.shop"

PostgreSQL extension of VS Code (Microsoft)

vscode-pgsql

pgsql.connections in the user settings; --editor cursor or windsurf for those editors

Reload the window

SQLTools

sqltools

sqltools.connections in the user settings, with an empty password

Reload the window

An export replaces only the connections it brings, found by a stable id or by name, and keeps the rest: what you made in the tool, and what an earlier export brought for other stands. Each changed file keeps a dated copy and its permissions, and a file that is a link stays a link. Connections DBeaver already has with the same host, port, database and user are left out, since the stands most often come from DBeaver in the first place. In ~/.pg_service.conf every exported section sits under the line # pg-mcp export: the next export rewrites only such sections, the server does not take them for stands, and a section of the same name that you wrote yourself stays as it is while its stand is left out. The TLS settings of a stand, sslmode and sslrootcert, go along. A settings file with comments is left alone, and the export prints what to add to it by hand. The PostgreSQL extension of Microsoft cannot read the password file: it asks for each password once and keeps it in the system keychain. Importing the file into pgAdmin again adds the servers again, so remove the old ones first. After import --write, and after registering an entry while stands exist, install.py at a terminal looks for these tools on the machine and offers to export into them.

3. Use. Every tool takes stand. Reading data also takes schema: the server connects with that schema's login and looks for unqualified names in it first. database is needed only when the schema exists in several databases of the stand.

You ask

The agent calls

"Which stands are there?"

status

"Is dev7 reachable?"

status with stand

"Which schemas are on dev7?"

list_schemas

"Where are the orders on dev7?"

search_schema with stand and text

"How many orders per status are in shop on dev7?"

run_select with stand and schema (profile analyst)

"On which dev stand is there an order 123?"

run_select with stands and schema

"Which stands have a discount column in shop?"

search_schema with stands and text

"How does shop on dev7 differ from dev12?"

compare_schema with stand_a, stand_b and schema

"Which dev stands differ from dev7 in shop?"

compare_schema with stand_a, stands and schema

Every answer starts with where it came from. Real answers on the demo database:

list_schemas with stand: dev7:

dev7 / demo: hr, shop

run_select with stand: dev7 and schema: shop:

dev7 / demo / shop
status | count
shipped | 174
paid | 162
cancelled | 90
new | 74
(4 rows)

compare_schema with stand_a: dev7, stand_b: dev12 and schema: shop, where dev12 is a copy of the demo database in which the price gained two digits, orders gained a note and a table of returns, and an index was dropped:

dev7 / demo / shop  vs  dev12 / demo / shop
only on dev12:
  shop.returns | table
differ:
  shop.order_items.price: dev7 numeric(10,2) NOT NULL | dev12 numeric(12,2) NOT NULL
  shop.orders.note only on dev12: text
  shop.orders: index orders_customer_idx only on dev7: CREATE INDEX orders_customer_idx ON shop.orders USING btree (customer_id)

compare_schema with stand_a: dev7, stands: dev* and schema: shop — the stands that match, each against dev7; up to three stands get the full differences, more stands get one line of counts each:

dev7 / demo / shop  vs  3 stands:
dev8 / demo / shop: same as dev7
dev9 / demo / shop: same as dev7
dev12 / demo / shop
only on dev12:
  shop.returns | table
differ:
  shop.order_items.price: dev7 numeric(10,2) NOT NULL | dev12 numeric(12,2) NOT NULL
  shop.orders.note only on dev12: text
  shop.orders: index orders_customer_idx only on dev7: CREATE INDEX orders_customer_idx ON shop.orders USING btree (customer_id)
same as dev7: 2; different: 1; not checked: 0

The structure comes from the system catalog of each stand, read with the schema's own login in three queries however many tables there are; the data is not read. A schema with no differences gets a single line, such as identical: 1 table, 3 columns, and a schema the registry lists but one stand's database lacks is an error, not a stand full of differences.

Without schema, list_tables, search_schema and describe_table cover the whole stand: the names come from the system catalog, which every login can see, through one login per database, and only the schemas of the registry are shown. The data stays behind each schema's own privileges: a login that cannot read a neighbouring schema gets permission denied there.

4. Several stands at once. search_schema and run_select take stands instead of stand: stand names separated by commas or a pattern, dev1,dev2,dev3 or dev*, at most 10 stands in one call. The stands are asked one after another, in the order status lists them, and the answer has a section per stand under its stand / database / schema line. A stand that is unreachable, has no login for the schema or rejects the query gets one line saying so under its header, and the other stands still answer. The 200 rows of run_select and the 100 matches of search_schema are shared among the stands, at least 10 per stand, so the whole answer fits the budget. The call keeps to 25 of the client's 30 seconds: when the stands asked so far have used most of it, the rest are named as not tried, and the agent asks for them in another call.

run_select with stands: dev*, schema: shop and SELECT count(*) FROM orders WHERE id = 123:

dev1 / demo / shop
count
1
(1 row)
dev2 / demo / shop
count
0
(1 row)
dev3 / demo / shop
  Connecting to dev3-db.example.com:5432 timed out. The host may be behind a firewall or a VPN.

A failure stays small. An unreachable stand or a broken login affects only the answers that need it, and status with stand tries a second login when the server turned the first one down. Without PG_MCP_STANDS, or when no stand is found, the entry works with the PG* variables as before, and status says why. At most 8 connections stay open (PG_MCP_MAX_CONNECTIONS). A host that does not answer is given up after 10 seconds and not tried again for the rest of that call, whatever else the stand has on it; a second login of a database is tried only when the server answered and turned the first one down. The reasoning is in ADR 0006 (in Russian).

Group profiles

The profile decides which tools the agent sees:

Profile

Tools

For

schema

status, list_tables, search_schema, describe_table

people who need the structure only: the agent sees no data rows at all

explore

plus sample_rows, explain_query

developers; the default

analyst

plus run_select and explain_query with ANALYZE

analysts who need their own SQL

Set it at registration (python3 install.py --profile schema) or with PG_MCP_PROFILE in the server's env block. An unknown value turns on the strictest profile, schema, and status warns about it. A profile is a convenience, not an access boundary: what can be read is decided by the role's privileges in the database.

Language

The server speaks English (en) or Russian (ru): tool descriptions, answers, hints, refusals and the output of install.py. The language is chosen like this:

  1. the PG_MCP_LANG variable (install.py --lang writes it);

  2. otherwise the system locale: the LANGUAGE, LC_ALL, LC_MESSAGES and LANG variables, and the regional settings on Windows;

  3. otherwise English.

status shows the current language. To change it, run python3 install.py --lang ru or edit PG_MCP_LANG in the settings, then restart the agent.

Tool and argument names, JSON-RPC protocol errors, the server log and PostgreSQL's own messages are not translated; the language of the latter is set by the database's lc_messages.

Either way, the model is asked to answer in the user's language. If your group's model picks tools less reliably from Russian descriptions, set PG_MCP_LANG=en: it will still answer in Russian. The reasons behind this design are in ADR 0004 (in Russian).

Security

Any change to data, structure, privileges, settings or other sessions is refused. There are three layers of defence, and each one holds even if the others miss something:

  1. The PostgreSQL role is the real boundary: it has only SELECT on the chosen schemas (a script for DBAs).

  2. A READ ONLY transaction for every query, always rolled back. A query goes out as a single statement, so PostgreSQL rejects a string such as SELECT 1; COMMIT; DROP ….

  3. A check of the query text lets only read-only statements through and refuses INSERT, UPDATE, DELETE, MERGE, SELECT … INTO, row locks and functions with side effects (pg_terminate_backend, set_config, dblink, pg_sleep and others), telling the model why.

The full policy is in ADR 0002 (in Russian).

Everything the tools return goes into the model's context. With a local model inside your organisation the data stays there, but pick the profile by the task: schema shows no data at all.

The server takes passwords from the password file, as psql does, and only from the line for its own login.

Query log

To keep a journal of what the agent asked the database, set PG_MCP_QUERY_LOG to an absolute file path (~ is fine) before running install.py, or add it to the server's env block later:

export PG_MCP_QUERY_LOG=~/pg-mcp-queries.jsonl
python3 install.py

Every tool call adds one JSON line. These two are real, from the demo database:

{"time": "2026-10-22T13:54:20.570Z", "os_user": "anna", "pid": 95715, "python": "3.9.6", "role": "mcp_demo_agent", "database": "pg_mcp_demo", "tool": "run_select", "arguments": {"query": "SELECT status, count(*) FROM shop.orders GROUP BY status"}, "outcome": "ok", "rows": 4, "duration_ms": 12.6}
{"time": "2026-10-22T13:54:20.571Z", "os_user": "anna", "pid": 95715, "python": "3.9.6", "role": "mcp_demo_agent", "database": "pg_mcp_demo", "tool": "run_select", "arguments": {"query": "DELETE FROM shop.orders WHERE status = 'cancelled'"}, "outcome": "refused", "message": "DELETE is not allowed. Only read-only statements are: SELECT, WITH ... SELECT, VALUES, TABLE, EXPLAIN and SHOW.", "duration_ms": 0.0}
  • outcome is ok, refused (by the SQL check), error (bad arguments or a PostgreSQL error), unavailable (no connection) or failed (a bug in the server);

  • rows is how many rows the agent's own statement returned (sample_rows, run_select, explain_query);

  • python is the version of Python the server runs on: the report below counts who runs which, which is what raising the minimum version would cost;

  • a call over several stands (stands) is still one line: stands maps each stand to its outcome (ok, unavailable, error, or skipped when the call had no time left for it), rows is the total, and role and database are empty, since every stand has its own;

  • the rows themselves and the tools' answers are never written.

The file is created readable by its owner only, since literals in SQL may carry personal data, and a symlink in its place is refused. It is opened anew for every line, so several agent sessions can share it and logrotate can move it away. If the file cannot be written, tool calls keep working and status says why.

To sum the log up for whoever looks after the group, run scripts/querylog_report.py with the files, rotated .1 and .gz ones too, and, if needed, a UTC window of --since and --until (a date or a date-time; a bare date in --until covers that whole day):

python3 scripts/querylog_report.py ~/pg-mcp-queries.jsonl ~/pg-mcp-queries.jsonl.1.gz --since 2026-10-20

The report counts calls by tool, by OS user and, in stands mode, by stand (a call over several stands counts on each), and the users on each version of Python; it gives the median and 95th percentile of duration_ms, the ten longest calls and every call the SQL check refused, which is what an attempt to write looks like. Arguments go on one line cut to 80 characters, values of keys that look like a password are hidden, and lines that are not complete log lines, say cut by a crash or by rotation, are skipped and counted:

Files: /home/anna/pg-mcp-queries.jsonl (13 lines, 1 broken)
Calls: 12  ok 8  error 1  refused 2  unavailable 1  failed 0
Period: 2026-10-22 09:12:03 .. 2026-10-22 16:41:05 UTC
Users: 2  Tools: 7
Duration: median 11.2 ms, 95th percentile 2003.5 ms

By tool
  tool            calls  ok  error  refused  unavailable  failed  problems
  run_select          5   2      1        2            0       0     60.0%
  list_tables         2   1      0        0            1       0     50.0%
  describe_table      1   1      0        0            0       0      0.0%
  explain_query       1   1      0        0            0       0      0.0%
  sample_rows         1   1      0        0            0       0      0.0%
  search_schema       1   1      0        0            0       0      0.0%
  status              1   1      0        0            0       0      0.0%

By user
  user   calls  queries  rows  refused
  anna       7        1     4        2
  boris      5        3    18        0

Longest calls (top 10)
  duration_ms  time                 user   tool            arguments
       2003.5  2026-10-22 16:30:00  anna   list_tables
         48.3  2026-10-22 14:05:11  boris  run_select      query=SELECT c.name, sum(o.total) AS total FROM shop.orders o JOIN shop.custome…
         35.0  2026-10-22 14:02:15  boris  search_schema   pattern=customer
  …

Refused by the SQL check (2)
  time                 user  tool        arguments
  2026-10-22 13:54:20  anna  run_select  query=DELETE FROM shop.orders WHERE status = 'cancelled'
  2026-10-22 16:41:05  anna  run_select  query=UPDATE shop.products SET price = price * 1.1

--format json gives the same figures to scripts, --format csv one table, the breakdown that --by user|tool|stand|day chooses.

The file lives on the user's machine, so it is a journal, not a tamper-proof audit. To audit data access, log on the PostgreSQL server (pgaudit or log_statement); the server's connections carry application_name = 'pg-mcp'. The reasoning is in ADR 0005 (in Russian).

Settings

All settings are environment variables in the env block of the pg server in the agent settings. install.py copies them there from the terminal.

Variable

Default

Meaning

PGHOST

localhost

database host, or the Unix socket directory (starts with /)

PGPORT

5432

port

PGDATABASE

same as PGUSER

database

PGUSER

the OS user name

role

PGSSLMODE

prefer

disable, allow, prefer, require, verify-ca, verify-full; only the last two check the certificate

PGSSLROOTCERT

—

CA certificate for verify-ca and verify-full

PGPASSFILE

~/.pgpass, on Windows %APPDATA%\postgresql\pgpass.conf

password file

PGPASSWORD

—

works, but install.py never copies it: the password belongs in the password file

PG_MCP_PROFILE

explore

schema, explore or analyst

PG_MCP_LANG

from the locale

en or ru

PG_MCP_MAX_RESPONSE_CHARS

8000

budget of one answer in characters, at least 500

PG_MCP_STATEMENT_TIMEOUT_MS

5000

timeout of one query in milliseconds

PG_MCP_LOG_LEVEL

INFO

detail of the stderr log: DEBUG, INFO, WARNING, ERROR

PG_MCP_QUERY_LOG

— (off)

absolute path of the query log file

PG_MCP_NAME

pg

the entry's name that status shows; install.py --name writes it

PG_MCP_SOCKET_TIMEOUT_S

30, for stands 10

seconds to wait for a new connection; once connected, the socket waits at least the query timeout plus 5 seconds

PG_MCP_STANDS

— (off)

* or stand name patterns such as dev*,ift2: one entry for many stands

PG_MCP_STANDS_FILE

~/.pg-mcp/stands.conf, on Windows %APPDATA%\pg-mcp\stands.conf

the stands registry

PGSERVICEFILE

~/.pg_service.conf, on Windows %APPDATA%\postgresql\.pg_service.conf

your psql services; stands are read from it too

PG_MCP_MAX_CONNECTIONS

8

connections an entry for stands keeps open

The budget counts characters. Russian text takes more tokens per character than English, so if the model's context is small, lower the budget rather than raise it.

For DBAs: a role for the agent

The docs/role.sql script creates a role that can only read the chosen schemas. Run it as a superuser or as the owner of the schemas, in the database the agent will read:

psql -d app -v role=mcp_agent -v password='…' -v schemas=sales,reports -v owner=app_owner -f docs/role.sql

Parameter

Meaning

role

name of the new role; PostgreSQL reserves names that start with pg_

password

its password; users put it into their password file

schemas

comma-separated schemas the agent may read

owner

the role that creates tables in those schemas, so future tables become readable too

The role gets LOGIN with no other attributes, at most 5 connections, default_transaction_read_only, statement (5 s) and idle-in-transaction (60 s) timeouts, CONNECT on the database, USAGE on the schemas, SELECT on all their tables, and default privileges on the owner's future tables.

Never grant this role pg_read_all_stats or pg_monitor (they show other sessions' query texts), pg_read_server_files, pg_write_server_files, pg_execute_server_program, pg_signal_backend, or membership in a role that owns tables.

Give each group its own role and its own set of schemas. For connections from outside, turn on TLS and ask users to set PGSSLMODE=verify-full.

Troubleshooting

Start with python3 install.py --check, or ask the agent to run /pg:setup: status nearly always says what is wrong.

What you see

What it means

What to do

The agent has no pg tools

the agent was not restarted, or the entry is missing

restart the agent; check that pg is in mcpServers; install.py --check

"Nothing accepts connections at …"

wrong host or port, the database is down, no VPN

check PGHOST, PGPORT and the VPN; compare with psql

"Host … cannot be resolved"

a typo in PGHOST, or a VPN is needed

fix PGHOST

"Connecting to … timed out"

a firewall or a VPN

check that the port is reachable from this machine

"… is unreachable from this machine"

no route to the host: the VPN is down

connect the VPN

"… accepted the connection, then closed it during the login"

the server refused the login, but its message was lost (Windows drops it on a reset); usually the password or pg_hba.conf

check the password file, then ask the DBA

"Login as … failed" or "the server asks for a password"

no password in the password file, a wrong line, or file permissions

check the path, the line format and chmod 600

"Database … does not exist"

wrong database name

fix PGDATABASE

"The server refused the login for …"

pg_hba.conf, or the role lacks LOGIN/CONNECT

ask the DBA

"TLS with … failed"

the TLS mode does not match the server

check PGSSLMODE and PGSSLROOTCERT

"The current role cannot read any table"

the role has no SELECT

ask the DBA: a role for the agent

settings.json "is not plain JSON"

the settings file has comments

add the printed snippet by hand

An answer ends with "… answer cut to"

the answer budget kicked in

narrow the question, or raise PG_MCP_MAX_RESPONSE_CHARS with the model's context in mind

python3 not found

Python is missing or has another name

python --version, on Windows py -3 --version; install Python 3.9+

"query log: …, not written: …"

the folder does not exist, no permission, a relative path, or a symlink in place of the file

fix PG_MCP_QUERY_LOG

"No stand … in the registry"

a typo, or the stand is missing from the registry or from PG_MCP_STANDS

status lists the stands; check the registry

"PG_MCP_STANDS is set, but no stands were found"

no registry file, or its sections are not named stand.database.schema

check the files the warning names

The server logs to stderr; PG_MCP_LOG_LEVEL=DEBUG adds detail. If the server does not answer the check, install.py --check prints that log.

Updating and removing

To update a clone, run git pull in the pg-mcp folder and restart the agent. To update an unpacked archive, take the new pg-mcp-<version>.zip from the Releases page, delete the old pg-mcp folder, unpack the new one in its place and restart the agent. Run install.py again only if the folder's path changed or you changed Python, the profile or the language.

To remove, delete pg (and any pg-<name> entries) from mcpServers in the agent settings (backups of earlier settings sit next to the file), run gigacode extensions uninstall pg-mcp and delete the folder.

FAQ

Does it need internet access, pip or admin rights? No. Everything the server needs is in the repository; it only needs a network path to the database.

Is this the pg-mcp package on PyPI? No, that is another project with the same name. This server is not on PyPI and installs only from GitHub, as a clone or a release archive (step 1): pip install pg-mcp and uvx pg-mcp install the other project.

Can the agent change anything in the database? No. Even if text inside the data tricks the model, the role's privileges, the read-only transaction and the text check stop the query.

Which PostgreSQL versions does it work with? CI tests it against PostgreSQL 13 to 18. Before 14, a table PostgreSQL has never analyzed looks exactly like an empty one, so list_tables calls both "empty or never analyzed".

Which MCP protocol versions does it speak? Revisions 2024-11-05 to 2025-11-25 (with the initialize handshake) and 2026-07-28 (without it).

Which clients is it checked with? CI starts Qwen Code, the open-source base of GigaCode CLI, with the server registered by install.py: the client must connect to the server and see the extension. GigaCode CLI itself is not available outside companies, so it is checked by hand before a release.

Will another MCP client do? Yes, any client that starts servers over stdio. install.py --client writes the entry for VS Code, Cursor, Windsurf, Cline, Roo Code, Continue, LM Studio, Claude Desktop and JetBrains (other agents and editors); for any other client, install.py --client cursor --print shows the command, the arguments and the PG* variables to give it.

Why does the agent answer in the wrong language? The model chooses the language of its answers; the server asks it to use the user's language. PG_MCP_LANG sets the language of the tools themselves.

Development

Environment, checks, code layout and rules are in CONTRIBUTING.md. Decisions and their reasons are in docs/adr/, the version history is in CHANGELOG.md. These documents are in Russian. How to report a vulnerability is in SECURITY.md.

License

pg-mcp is under the MIT license (LICENSE). The packages it ships unchanged in src/pg_mcp/_vendor keep their own licenses, listed in THIRD_PARTY_NOTICES.md.

Available Tools

6 tools
describe_tableA
Read-only

Columns with types, NOT NULL, defaults and comments; keys, referencing tables, indexes.

NameRequiredDescriptionDefault
tableYes"orders" or "sales.orders".
schemaNoSchema to look in; omit for every readable schema.

TDQS

A3.5/5.0
Behavior4/5

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

Annotations already declare readOnlyHint=true and openWorldHint=false, so the safety profile is covered. With no output schema, the description earns credit for itemizing the return payload (columns, keys, referencing tables, indexes), which is real information an agent cannot get from structured fields. It does not say what happens for a nonexistent or ambiguous table, 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.

Conciseness4/5

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

A single dense clause with no filler, front-loading the most commonly needed information (columns with types). It is a sentence fragment rather than a structured statement of purpose, which slightly limits immediate scanability.

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 low-complexity, two-parameter read-only tool with full schema coverage, the description covers what the caller needs to know about the return contents even without an output schema. Missing only edge-case behavior such as errors for missing tables and whether output is truncated for very wide tables.

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 both 'table' (accepting 'orders' or 'sales.orders') and 'schema' documented in the schema itself. The description adds nothing about parameter behavior or interactions, 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?

The description enumerates exactly what the tool retrieves: column types, NOT NULL flags, defaults, comments, keys, referencing tables and indexes. It is clearly distinct from siblings like list_tables, search_schema and sample_rows. It stops short of a stated verb+resource framing, relying on the tool name to supply 'describe', so it is clear but not fully self-contained.

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 explicit guidance on when to use this versus search_schema or sample_rows, and no stated preconditions. The schema hint about omitting 'schema' to scan every readable schema is the only usage-adjacent signal, and that lives in the parameter schema rather than the description.

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

explain_queryA
Read-only

PostgreSQL's plan for one read-only query passed without EXPLAIN; the query is not run.

NameRequiredDescriptionDefault
queryYesOne SQL statement.

TDQS

A3.7/5.0
Behavior4/5

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

Annotations already cover safety (readOnlyHint=true, openWorldHint=false), yet the description adds genuinely non-redundant behavior: the caller must not prefix EXPLAIN themselves, and the query is not executed, so there are no side effects and no ANALYZE-style execution. That is exactly the kind of context annotations cannot convey.

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?

A single compact sentence with no waste, and the key 'not executed' fact lands early via the semicolon. The pre-semicolon clause is awkwardly constructed ('plan for one read-only query passed without EXPLAIN'), costing a bit of parseability.

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

Completeness3/5

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

For a one-parameter read tool with annotations present, the description covers the essentials of what to pass and what will not happen. With no output schema, however, it says nothing about the return shape (plan node rows/text) or error behavior for invalid or write statements, which an agent would benefit from knowing.

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% ('One SQL statement.'), so the baseline is 3, but the description adds a real constraint the schema omits — the statement must be read-only and only one statement is accepted. It stops short of noting dialect-specific restrictions or what happens with DML input.

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 resource — PostgreSQL's execution plan for a single query — and states a key behavioral fact (no EXPLAIN needed, query not executed). It is not tautological with the name, though the phrasing 'plan for one read-only query passed without EXPLAIN' is grammatically clunky and no sibling tool is mentioned for contrast.

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 rather than stated: an agent must infer that this is the tool for inspecting a query's plan before/without running it. The 'read-only' and 'not run' constraints implicitly exclude write statements, but there is no explicit when-to-use, when-not, or pointer to siblings like sample_rows or describe_table for data inspection.

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

list_tablesA
Read-only

Tables, views and foreign tables the role can read, with row estimates and comments. At most 200; pass schema to narrow.

NameRequiredDescriptionDefault
schemaNoOnly list objects in this schema; omit it to list every schema.

TDQS

A3.9/5.0
Behavior4/5

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

Annotations already declare readOnlyHint=true and openWorldHint=false, so safety is covered. The description adds genuinely new behavioral facts: the listing is permission-scoped ('the role can read'), returns row estimates and comments, and is capped at 200 results — a real limit the agent must plan around. It stops short of saying whether exceeding 200 truncates silently or errors.

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 output contents are front-loaded ahead of the cap and the parameter hint. Every clause carries 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?

No output schema exists, and the description compensates by naming what comes back (row estimates, comments). For a one-parameter read-only listing tool that is sufficient; the only remaining ambiguity is the truncation behavior at the 200 cap.

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?

With one parameter and 100% schema coverage, the schema already defines 'schema'. The description adds meaning beyond it by tying the parameter to the 200-result cap — narrowing the schema is presented as the mitigation for the limit, which is a usage-level insight the schema doesn't convey.

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 resource (tables, views, foreign tables) with a scope qualifier ('the role can read') and even names the payload fields (row estimates, comments). It doesn't explicitly contrast itself with describe_table or search_schema, but an agent can tell this is the enumeration tool.

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?

'At most 200; pass schema to narrow' implies the use case (browse an inventory, narrow by schema) and surfaces the result cap, but there is no explicit when-to-use guidance versus siblings like search_schema or describe_table, and nothing about when-not to call it.

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

sample_rowsA
Read-only

A few rows of a table or view, optionally some columns. Default 10, at most 50, in no order.

NameRequiredDescriptionDefault
limitNoHow many rows, 1 to 50. Default 10.
tableYes"orders" or "sales.orders".
schemaNoSchema to look in; omit for every readable schema.
columnsNoColumns, e.g. "id, total"; omit for all.

TDQS

A3.6/5.0
Behavior4/5

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

Annotations already declare readOnlyHint=true and openWorldHint=false, so the safety profile is covered. The description adds useful behavioral context: the default row count, the hard cap of 50, and that rows come back in no guaranteed order. It does not cover errors or permissions, but the annotations lower the bar.

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?

It is two short sentences with zero waste, front-loading the core purpose and then the key limits. Every phrase contributes to understanding the tool.

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

Completeness4/5

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

For a simple read-only sampling tool with full schema coverage and clear annotations, the description is largely complete. The only meaningful gap is the lack of explicit selection guidance against sibling 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%, so the schema already documents all four parameters thoroughly. The description restates the limit default/cap and the optional columns, but adds little parameter meaning beyond what the schema provides; the 'in no order' remark is behavioral rather than parameter-specific.

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 clearly identifies the resource (rows of a table or view) and the sampling scope (a few rows, optional columns). It does not explicitly distinguish this tool from siblings like describe_table or list_tables, so it falls 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 Guidelines2/5

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

The description implies usage by saying it returns a few rows, but it gives no explicit guidance on when to use this instead of alternatives such as describe_table, search_schema, or list_tables. There are no when-to-use or when-not-to-use conditions.

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

search_schemaA
Read-only

Find tables, views and columns by a piece of name or comment; use it instead of paging list_tables. At most 100 matches.

NameRequiredDescriptionDefault
textYesPart of a name or comment, e.g. "order".
schemaNoSchema to look in; omit for every readable schema.

TDQS

A4/5.0
Behavior3/5

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

Annotations already declare readOnlyHint=true and openWorldHint=false, so safety is covered. The description adds one genuinely useful behavioral fact, the 100-match cap, but says nothing about ordering, truncation signaling, or what a match record contains. One extra detail against an already-declared safety profile lands at a 3.

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 front-loads what is searched, then tacks on the routing hint and the result cap. No filler, nothing 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?

No output schema exists, so the description should describe results; it names the resource types returned and the cap, which covers the essentials. Ordering and whether the cap is signaled to the caller are the remaining gaps.

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, including the "omit for every readable schema" default, are already documented in the schema. The description's phrasing of "a piece of name or comment" restates the parameter without adding substring/case-sensitivity semantics.

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 (find) plus the exact resources searched (tables, views, columns) and the match key (name or comment). It also names the sibling it replaces for this job, so an agent can separate it from list_tables without reading either schema.

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?

"Use it instead of paging list_tables" gives an explicit alternative and the condition that selects it. It stops short of covering the other siblings (describe_table, sample_rows) which would be the natural follow-ups once a match is found.

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

statusA
Read-only

Read-only server: database, role, profile and limits. Call it first, and when a tool reports a connection problem: it says what to fix.

NameRequiredDescriptionDefault

No parameters

TDQS

A4.4/5.0
Behavior4/5

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

Annotations already establish read-only and non-open-world behavior, and the description adds useful context that it reports database, role, profile, limits, and says what to fix on connection issues. It does not detail the exact response shape or troubleshooting scope, but it adds meaningful behavioral guidance beyond the annotations.

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

Conciseness5/5

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

The description is two short sentences with no wasted text. The initial-call guidance and connection-failure use case are front-loaded and easy to scan.

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

Completeness5/5

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

For a zero-parameter status tool with annotations and no output schema, the description is complete enough: it says what information is returned and when to invoke it. No additional schema or parameter explanation is needed.

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

Parameters4/5

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

The tool has zero parameters, so there is no parameter semantics to explain. The schema is fully documented and the description appropriately does not spend space on nonexistent inputs.

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 clearly identifies this as a read-only server-status tool that reports database, role, profile, and limit information. It distinguishes itself from schema-oriented siblings by framing its role as the first call and as a connection-problem diagnostic, though it lacks an explicit verb like "return" or "get."

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?

It gives explicit usage guidance: call it first, and call it when another tool reports a connection problem. These are the key moments an agent needs to select this tool, and no alternative tool is appropriate for this diagnostic role.

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. 1 tool updatev0.6.0
    • Changedlist_tables1 field changed
      • changedInput schema / properties / schema / description
        Previous value: -"Only list objects in this schema, e.g. \"public\". Omit to list all schemas."New value: +"Only list objects in this schema; omit it to list every schema."
  2. 6 tool updatesv0.5.0
    • First observeddescribe_table
    • First observedexplain_query
    • First observedlist_tables
    • First observedsample_rows
    • First observedsearch_schema
    • First observedstatus

TDQS

A3.9/5.0

Scored across 6 tools

Disambiguation4/5

The majority of tools are distinct, separating schema listing, searching, detailing, data sampling, and query planning. However, `list_tables` and `search_schema` share a similar purpose of object discovery, which could occasionally cause agent confusion.

Naming Consistency5/5

The tool names exhibit a very consistent naming convention, generally adhering to the action_object format using snake_case. The naming pattern is immediately recognizable and predictable across the entire tool set.

Tool Count4/5

With 6 tools, the surface is well-scoped for a read-only PostgreSQL introspection server. Each tool performs a distinct function, contributing meaningfully to database inspection without being overly verbose or incomplete.

Completeness4/5

The tool set provides comprehensive coverage for read-only operations, including health checks, schema discovery, structural inspection, data sampling, and query planning. Minor gaps remain but do not prevent an agent from performing core database tasks.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides read-only access to PostgreSQL databases, enabling LLMs to inspect database schemas and execute read-only SQL queries within a secure transaction context.
    127,685 npm
    1
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides read-only access to PostgreSQL databases, enabling LLMs to inspect database schemas and execute read-only SQL queries.
    127,685 npm
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables secure read-only access to PostgreSQL databases, allowing users to list tables, query schemas, execute SELECT statements, and inspect table structures through natural language interactions.
    681 npm
    4
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables read-only PostgreSQL database access with query, table listing, and schema inspection tools.
    ISC