Skip to content
Conduit

mcp server

pgglass

Read-only PostgreSQL MCP server for AI agents: production behind glass. Many databases in one call, bounded output, audited.

Jpgomesf/pgglasscommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

This project does not publish an install method we can turn into a config. Follow the README instead.

README

synced Oct 21, 2026

  • Read-only, enforced by the server. Every statement the agent sends is parsed with the Postgres parser and must be one SELECT, EXPLAIN of one or SHOW. It runs in a READ ONLY transaction 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_query runs a statement on up to 10 connections in parallel and merges their rows under a connection column. pg_diff_schema lists 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 id on 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:

  1. 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
    
  2. 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"
        }
      }
    }
    
  3. Add it to Claude Code:

    claude mcp add --transport stdio --scope user pgglass -- ~/.local/bin/pgglass
    
  4. Check the config and the connection's role:

    ~/.local/bin/pgglass doctor
    

    It 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

ToolDescription
pg_list_connectionsList the configured connections and their role-check status
pg_schemaDescribe a database step by step: schemas, then relations, then one relation in detail
pg_searchFind relations, columns and functions by name
pg_queryRun one read-only statement on one connection, or on up to 10 in parallel, and return its rows as TSV, JSON or Markdown
pg_explainSummarize the plan of a SELECT, with EXPLAIN ANALYZE on request, and flag large sequential scans, misestimates, spills to disk and large nested loops
pg_profileProfile a table's columns from pg_stats: nulls, distinct values, most common values, range; when pg_stats has nothing on them, from a sample of at most 10,000 rows
pg_diff_schemaCompare one schema on two connections: relations, view queries, columns, constraints, indexes and RLS policies
pg_healthCheck a server: connections, long-running and blocked sessions, cache hit ratio, dead rows, unused indexes, transaction ID wraparound and replica lag

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_schema lists 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).

pgglass 1.0.0Postgres MCP Pro 0.3.0DBHub 1.5.0MCP Toolbox 1.14.0Reference server 0.6.2, deprecated
DatabasesPostgreSQLPostgreSQLPostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLitePostgreSQL, AlloyDB, BigQuery, MySQL and many morePostgreSQL
Read-onlyAlways: each statement parsed with the Postgres parser and run in a READ ONLY transaction that is rolled back; the role audited on first useWith --access-mode=restricted: read-only transactions under a time limit, and SQL parsed with pglast so that only read-only statements passWith readonly = true on a tool (default false): a keyword allow-list, then a READ ONLY transactionRead-only mode on Cloud SQL, AlloyDB and BigQuery sources; the postgres source has no such settingEvery query in a READ ONLY transaction that is rolled back
Databases per serverSeveral; one pg_query call runs on up to 10One, DATABASE_URISeveral, as [[sources]] in dbhub.tomlSeveral, as sources in tools.yamlOne, given as its argument
Tools, tools/list bytes8, 5,4849, 5,2892, 1,47929, 20,693 (prebuilt postgres)1, 131

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.

  1. 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.
  2. The role is checked. On a connection's first use, the server audits what its role can do beyond reading, and role_check: strict refuses one with a critical finding.
  3. SQL is parsed, not pattern-matched. pg_query accepts one SELECT, EXPLAIN of one or SHOW, checked with the Postgres parser, and rejects a denylist of functions and catalogs.
  4. Every statement runs read-only and is rolled back. Each runs alone in a READ ONLY transaction that is always rolled back, with statement_timeout and lock_timeout set.
  5. Results are bounded. Rows past max_rows or max_bytes are not kept, and a result past 10 KB is written to a private CSV file and summarized.
  6. Calls are audited. Each call writes one JSON line to stderr; SQL text, parameters and values are never logged.
  7. HTTP stays on this machine unless told otherwise. --http listens on loopback by default, any other address needs a bearer token, and requests from web pages on other origins are refused.
  8. A connection can ask first. With confirm set, the user sees the statement and its params through 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 dblink or 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_timeout ends it.
  • --http serves 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.
  • --user runs the server as you, so it can read a config.json only you can read.
  • Credentials resolve inside the container: pass the variables that env sources read with -e, and keep .env files 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/pgglass and 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.

Documentation

DocDescription
Usage GuideTools, arguments, formats, large results, errors
ConfigurationCredential sources, connection options, defaults, PgBouncer, command line, doctor, Streamable HTTP
Safety modelThe safety model in full: each layer, and what the layers do not cover
How it's builtThe packages, the path of one tool call through them, and how pgglass is tested
Token benchmarkWhat pgglass puts in an agent's context, measured, and how other servers' tool lists compare
ExtendingHow to add custom credential sources (Vault, GCP, etc.)
UpgradingMoving to pgglass from its earlier name: config location, client entry, permission rules, spill files
ContributingBranch strategy, commit conventions, development workflow
ReleasingWhat a release publishes, cutting one, the MCP Registry, platforms

License

MIT

↑↓ navigate openesc close