- Read-only, enforced by the server. Every statement the agent sends is parsed with the Postgres parser and must be one
SELECT,EXPLAINof one orSHOW. It runs in aREAD ONLYtransaction that is always rolled back, under a statement timeout, and each connection's role is audited on first use. See the safety model. - Many databases, one call.
pg_queryruns a statement on up to 10 connections in parallel and merges their rows under aconnectioncolumn.pg_diff_schemalists what differs in a schema between two connections, such as production and staging. - Frugal with tokens. The eight tool definitions take 5,484 bytes of
tools/list, and a session that loads them and asks ten typical questions an estimated 2,550 tokens. Output is compact text, TSV by default, and plans, column statistics and server health come back as a few summary lines, not raw dumps. A result past 10 KB is written to a private CSV file and summarized, so the agent reads or greps the file instead of the rows:SELECT * FROM events ORDER BY idon a 20,000-row table comes back in 963 bytes, with its first 1,000 rows (max_rows) in the file. The token benchmark measures this, and fails CI when a response grows more than 10%.
Quickstart
On macOS or Linux:
-
Install the latest release in
~/.local/bin, once its SHA-256 matches the release's checksums:curl -fsSL https://raw.githubusercontent.com/Jpgomesf/pgglass/main/install.sh | sh -
Create
~/.config/pgglass/config.json, with a role that can only read:{ "connections": { "my-db": { "source": "direct", "dsn": "postgres://readonly_user:password@localhost:5432/mydb" } } } -
Add it to Claude Code:
claude mcp add --transport stdio --scope user pgglass -- ~/.local/bin/pgglass -
Check the config and the connection's role:
~/.local/bin/pgglass doctorIt prints a line per connection, such as
ok my-db: role readonly_user: no findings, and exits 1 if one fails.
Then ask your agent about the database, such as "which tables in my-db are the largest?". Install has the release archive, Docker and source builds; Configure has the other credential sources.
Tools
Calls run in parallel, across connections and on the same one. The server and each tool have a display title, and on connecting the server gives the agent short instructions on using the tools well. It serves over stdio, or over Streamable HTTP with --http, speaks MCP revisions 2024-11-05 to 2026-07-28 and settles on the newest the client speaks too. See the usage guide.
Tables are also MCP resources, pgglass://<connection>/<schema>/<table>, read through the same role check, limits and audit log as the tools: in Claude Code, @-mention one to add it to a prompt as pg_schema describes it. The server lists the tables of the connections the agent has used, so listing them opens no other connection, and the listing keeps to 10 KB. See resources.
Many databases, one call
In place of connection, pg_query takes connections, up to 10 names, and runs the statement on each in parallel:
{"connections": ["staging-app", "prod-app"], "sql": "SELECT count(*) FROM orders"}
connection count
staging-app 1204
prod-app 98311
(2 rows)
- The statement is checked once. Each connection then gets what a call on it alone would: the role check, its
search_path, the limits and one audit line. - A connection that fails adds a line with its error after the rows; the call fails only when every connection does.
- To compare structure rather than rows,
pg_diff_schemalists what differs in one schema between two connections.
See Fan-out and Compare a schema across connections.
How it compares
Other Postgres MCP servers, as their own READMEs and docs describe them at the version named, read on 2026-10-14. The tools/list sizes were measured on 2026-10-13, each server in its published version against the same fixture (method).
Each of them does more than this table shows, such as Postgres MCP Pro's index tuning, DBHub's web workbench and MCP Toolbox's custom tools: follow the links. The token benchmark also measures the size of pgglass's responses, which tools/list says nothing of.
Safety model
The calling agent is treated as untrusted: instructions can reach it through the data it reads (prompt injection). These layers hold whatever SQL it sends. The safety model has each in full.
- The database role is the real boundary. The server never gives the agent more than the role it connects as: connect as a role that can read only what the agent should see.
- The role is checked. On a connection's first use, the server audits what its role can do beyond reading, and
role_check: strictrefuses one with a critical finding. - SQL is parsed, not pattern-matched.
pg_queryaccepts oneSELECT,EXPLAINof one orSHOW, checked with the Postgres parser, and rejects a denylist of functions and catalogs. - Every statement runs read-only and is rolled back. Each runs alone in a
READ ONLYtransaction that is always rolled back, withstatement_timeoutandlock_timeoutset. - Results are bounded. Rows past
max_rowsormax_bytesare not kept, and a result past 10 KB is written to a private CSV file and summarized. - Calls are audited. Each call writes one JSON line to stderr; SQL text, parameters and values are never logged.
- HTTP stays on this machine unless told otherwise.
--httplistens on loopback by default, any other address needs a bearer token, and requests from web pages on other origins are refused. - A connection can ask first. With
confirmset, the user sees the statement and itsparamsthrough the client, and it runs only if they accept.
What these layers do not cover, in short:
- The parser knows names, not what is behind them, and the denylist holds only known functions: a function the role may execute can call
dblinkor another escape internally, and a view over a denied catalog reads it. - Whatever the role can
SELECT, the agent can read and repeat, including what Postgres shows the role about the server, such as settings and other sessions' SQL. - A superuser connection has no role boundary: use
role_check: strict, or a role that is not a superuser. - A query may use the database's CPU and I/O, and hold locks that delay DDL, until
statement_timeoutends it. --httpserves plain HTTP: to reach it from elsewhere, put a proxy that terminates TLS in front of it, and set the token.
Install
pgglass runs on macOS and Linux, amd64 and arm64, and supports PostgreSQL 15, 16, 17 and 18, which CI tests each. Windows is not supported: see Platforms.
Install script
curl -fsSL https://raw.githubusercontent.com/Jpgomesf/pgglass/main/install.sh | sh
The script downloads the latest release for your platform, checks its SHA-256 against the release's checksums.txt, and installs pgglass in ~/.local/bin only if they match. To pick a release or a directory, set PGGLASS_VERSION or PGGLASS_INSTALL_DIR for sh: ... | PGGLASS_VERSION=v1.0.0 sh.
Release archive
Download your platform's pgglass_<version>_<os>_<arch>.tar.gz and checksums.txt from Releases, verify the archive, then unpack the binary:
sha256sum --ignore-missing -c checksums.txt
gh attestation verify pgglass_1.0.0_linux_amd64.tar.gz --repo Jpgomesf/pgglass
tar -xzf pgglass_1.0.0_linux_amd64.tar.gz pgglass
gh attestation verify checks that the release workflow of this repository built the archive.
Docker
The image ghcr.io/jpgomesf/pgglass (linux/amd64 and linux/arm64, distroless, non-root) serves stdio. Mount the directory holding config.json read-only:
docker run --rm -i --user "$(id -u):$(id -g)" \
-v ~/.config/pgglass:/etc/pgglass:ro \
ghcr.io/jpgomesf/pgglass --config /etc/pgglass/config.json
- Give your MCP client that command, as in
claude mcp add pgglass -- docker run --rm -i ..., and pin a version with a tag such as:1.0.0. --userruns the server as you, so it can read aconfig.jsononly you can read.- Credentials resolve inside the container: pass the variables that
envsources read with-e, and keep.envfiles in the mounted directory. - A result past 10 KB is written to a file inside the container, out of the agent's reach. To share these files, run
mkdir -p ~/.cache/pgglassand add-v "$HOME/.cache/pgglass:$HOME/.cache/pgglass" -e XDG_CACHE_HOME="$HOME/.cache": the directory has the same path on both sides, so the agent can open the path the server reports.
export PGGLASS_HTTP_TOKEN="$(openssl rand -hex 32)"
docker run --rm -p 127.0.0.1:8411:8411 -e PGGLASS_HTTP_TOKEN --user "$(id -u):$(id -g)" \
-v ~/.config/pgglass:/etc/pgglass:ro \
ghcr.io/jpgomesf/pgglass --config /etc/pgglass/config.json --http 0.0.0.0:8411
In the container the server has to listen on every interface, which requires the token; publishing the port on 127.0.0.1 keeps it on this machine. Clients connect to http://127.0.0.1:8411/mcp with the header Authorization: Bearer $PGGLASS_HTTP_TOKEN.
From source
go install github.com/jpgomesf/pgglass@latest
Or clone the repository and run make build.
Configure
Create ~/.config/pgglass/config.json ($XDG_CONFIG_HOME/pgglass/config.json when XDG_CONFIG_HOME is set to an absolute path), or pass another file with --config:
{
"defaults": {
"statement_timeout_seconds": 30,
"role_check": "strict"
},
"connections": {
"my-db": {
"source": "direct",
"dsn": "postgres://readonly_user:password@localhost:5432/mydb"
}
}
}
Credentials come from direct, env, dotenv or aws_secrets_manager. See Configuration for every setting. Then check the config and each connection's role:
$ pgglass doctor
config /home/me/.config/pgglass/config.json (role_check strict)
ok my-db: role readonly_user: no findings
0 failed, 1 passed
It exits 1 when a connection's credentials do not resolve, its server does not answer, or its role has a way out of the read-only transaction (see Doctor).
Add to Claude Code
Over stdio, Claude Code starts the server itself:
claude mcp add --transport stdio --scope user pgglass -- /path/to/pgglass
Put --config /path/to/config.json after the binary for a config file elsewhere. Over Streamable HTTP, start the server, then add its URL:
pgglass --http :8411
claude mcp add --transport http --scope user pgglass http://127.0.0.1:8411/mcp
--scope user adds the server to every project; leave it out for the current project alone, or use --scope project to share it in the project's .mcp.json. Other MCP clients take the same command or URL. To serve beyond this machine, with a bearer token, see Streamable HTTP.
Upgrading from mcp-postgres? Upgrading lists what to move and rename.