One read-only PostgreSQL MCP server for every database you run.
pg-licht gives an AI assistant 76 tools and 13 guided investigations for exploring schemas, reading statistics and diagnosing live servers. It can never change your data, it never returns rows from your tables, and it answers for a whole fleet from a single process.
One server for the whole fleet
Every connected tool takes a connection argument, so one
pg-licht answers for one database or for hundreds. Connections are named once, in one
file, and opened only when a call needs them.
A hundred databases, one entry in your MCP client
- One process, one connections file, credentials kept in libpq's own
.pgpassand service files. - One tool listing in the agent's session. Its size is the same with 1, 150 or 1,000 connections configured, and startup stays under a tenth of a second.
- Connections open when a call needs them; up to 32 idle ones stay warm, and the least recently used is closed first.
- Ask a replication group, an instance or any labelled group in one call: up to 32 members per call, 16 at a time, one result per member, and any beyond 32 listed as skipped rather than silently left out.
A hundred entries, a hundred sets of settings
- A hundred processes to start, update and keep logged in.
- A hundred copies of the same tools, each under a different server name, for the agent to tell apart.
- A client that loads tool definitions up front carries every copy in the session.
- No way to ask the same question of a replica and its primary in one call.
Measured with 1, 150 and 1,000 configured connections. One caveat worth
knowing: listConnections is roughly 400 bytes a connection, so on a large
fleet ask for the ones you want with pattern. listConnections
and listTopology both take one, matched against names and labels.
Read-only by construction, not by convention
Safety is enforced by PostgreSQL on every call, not left to how the assistant behaves.
A read-only transaction per call
Every call to a database runs inside its own READ ONLY transaction. A
write, even one reached through a bug, fails in PostgreSQL rather than succeeding. The
pooler tools send PgBouncer only SHOW, as the stats user you give them,
which PgBouncer allows nothing else.
A timeout on every statement
statement_timeout is set on the same transaction, two minutes by
default and set per connection, so no call can hold a backend indefinitely.
No SQL built from arguments
Catalog queries are parameterized. No schema, table or search term is ever concatenated into SQL text.
Plans are proven before anything runs
explainQuery executes a statement only after its plan is shown to
contain no write, and only within a memory and CPU budget taken from the host you
declared.
Safe behind PgBouncer
The guards are transaction-scoped, so they hold under transaction pooling. The whole suite runs through a real PgBouncer on every change.
Honest about privileges
checkPrivileges says which tools the connecting role can really use:
59 of 73 database tools for a bare login role, 67 with pg_monitor.
It returns structure, not your rows
pg-licht describes structure and statistics. It is built so that an assistant can understand a database without seeing what is in it, and it is exact about the few places where values can still appear.
Never
- Writes, alters or deletes anything.
- Returns rows from a table. The three tools that read one return only a yes or no
(
checkKey), counts of rows and pages (rowScatter) or a plan with row counts (explainQuery). - Returns a password it holds, a user mapping's options, or a subscription's connection string.
- Returns a constant
pg_qualstatsrecorded. It keeps the literal of every predicate it samples;predicateStatsandsuggestIndexesreadtable.column = ?, and a test fails the build if a seeded literal ever appears in either.
Where values can appear (14 tools, each marked)
- Statement text: what sessions are running now, and what
pg_stat_statementsrecorded. - Column statistics: most common values and histogram bounds.
- Plans and definitions, which repeat the literals written into them.
- Settings, such as a standby's
primary_conninfo.
Each of these is limited further by PostgreSQL's own permissions: a role
without SELECT on a column gets none of its statistics, and a role without
pg_read_all_stats sees only its own statements. Connect as the narrowest
role that answers the question. The full list is under
What reaches the caller in the manual.
Built for the questions operators actually ask
Guided investigations
13 prompts walk the assistant through slow queries, lock contention, deadlocks, bloat, disk space, replication slots and schema changes, tool by tool.
Beyond what PostgreSQL measures
Where pg_wait_sampling, pg_stat_kcache and
pg_qualstats are installed: what the server has been waiting on, the CPU
and real disk reads behind each statement, which predicates throw rows away, and the
index that would serve them — all joined on the query_id
statementStats reports, and each suggestion ready to test with
evaluateIndex without building it. A counter the server's platform
does not measure is reported as unavailable, with the reason, never as zero.
Answers you can cap
The listings and searches that grow with a database take a pattern or
a narrower search, and the largest answers narrow themselves by default. Set a cap in
budgets.ini and an answer past it is refused with the arguments that
make it smaller, rather than returned.
The pooler too
Point a section at PgBouncer's admin console and ask whether the pools keep up:
clients waiting for a server and for how long, servers in use against each pool's
size, and the connections and settings behind them — read with
SHOW alone, as the stats user you give them.
Topology aware
Label connections by instance and replication group. Roles are observed on each
call, never configured, and verifyTopology checks the labels against the
servers.
Answers a client can check
Every tool declares its output schema and is marked read-only, and clients on MCP 2025-06-18 or later receive structured results.
Tested where it matters
PostgreSQL 14 through 18, under AddressSanitizer, UndefinedBehaviorSanitizer, ThreadSanitizer and valgrind, through a pooler, with a standby, a cascading standby, a logical subscriber and a split brain, built with both g++ and clang, and on FreeBSD 14 and 15. The protocol and both configuration parsers are fuzzed on every change, and clang-tidy fails the build on any finding.
Packages that are installed before they ship
Every .deb and .rpm is installed on the distribution it
targets and run before a release is published, its version checked against the
binary inside it, and the Homebrew formula is built from source on every change.
Downloads you can verify
Every release asset carries signed build provenance:
gh attestation verify proves a file was built by this repository's
release workflow from the tagged commit. A SHA256SUMS file is published
beside them for a check that needs no GitHub CLI.
Hardened binaries
The Linux binaries are position-independent, with full RELRO, a non-executable stack, stack protection and fortified libc calls — and each release binary is inspected for them before it ships, rather than trusting the flags that were asked for.
76 tools, grouped by what they answer
- Privileges4
- Schema exploration19
- Catalog search3
- Cluster-wide objects7
- Extensibility and text search4
- Foreign data and replication7
- Monitoring and statistics17
- Diagnostics and query planning9
- Topology2
- Connections1
- Connection poolers3
Two arguments narrow an answer by a string. The listings take
pattern, a literal substring of a name: user_ finds
user_x, not users. The three search tools take
web_search, full-text search over names, comments and source, where
order finds orders but ord finds nothing.
Which tool takes which.
Install
brew tap sqlambda/pg-licht
brew install pg-licht
claude mcp add --transport stdio pg-licht \
-e DATABASE_URL="postgresql://user@host/db" \
-- pg_licht_mcp
# ~/.config/pg_licht/connections.ini
[billing_prod]
service = billing_ro
instance = pg-prod-01
[billing_replica]
service = billing_replica_ro
replication_group = billing-ha
Debian 13 and Rocky Linux 9 packages and tarballs are on the releases page; see INSTALL for each.