readonly-postgres-mcp
Offers a NestJS module for dependency injection of the readonly PostgreSQL client in NestJS applications.
Provides read-only access to PostgreSQL databases with SQL guardrails for analytical queries and schema introspection.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@readonly-postgres-mcp list all tables in the public schema"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
readonly-postgres-mcp
Let AI read your PostgreSQL database - without letting it write to it.
One MCP server. One job. Read PostgreSQL safely.
This package never writes to the database. There is no write API and no
migration runner - not a mode that is switched off, but code that does not
exist. Three independent layers enforce it: a SQL guard, a read-only
transaction, and a database role granted SELECT and nothing else.
Read SECURITY.md for the threat model and the guard's documented limits before pointing this at production.
Install from npm
npm install readonly-postgres-mcpOr run the MCP server without a global install:
npx readonly-postgres-mcpOptional peer for NestJS apps:
npm install @nestjs/commonRelated MCP server: mcp-postgres
Environment
A connection URL works, if you already have one:
DATABASE_URL=postgresql://readonly_user:pw@db.example.com:5432/analytics?sslmode=requirePG_URL is also accepted and takes precedence. Any discrete PG_* variable
overrides the matching part of the URL.
Or set the parts individually:
PG_HOST=localhost
PG_PORT=5432
PG_DATABASE=postgres
PG_USERNAME=readonly_user
PG_PASSWORD=...
PG_SSL_MODE=require
PG_SEARCH_PATH=public
PG_STATEMENT_TIMEOUT_MS=30000
PG_MAX_ROWS=10000
PG_MCP_ALLOW_ADHOC=true
PG_ALLOW_EXPLAIN_ANALYZE=falseVariable | Default | Purpose |
| - | Required unless a connection URL is set |
|
| |
| see TLS | libpq |
|
| Comma-separated schemas |
|
| MCP tools use 15000 |
|
| MCP tools use 1000 |
|
|
|
|
|
|
| - | Path to your own |
Optional (backward-compatible fallbacks): PG_SSL (boolean) and
PG_SSL_REJECT_UNAUTHORIZED (boolean, overrides certificate verification for
whatever mode is resolved).
TLS
PG_SSL_MODE is a discrete connection parameter, set the same way as PG_HOST,
PG_DATABASE, PG_USERNAME and PG_PASSWORD - no connection string required.
It accepts the same values as a libpq sslmode= parameter:
| Pool |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Unset: TLS is disabled for localhost / 127.0.0.1 and enabled without
certificate verification for any other host.
Use a dedicated database role with SELECT only. See docs/db-role.sql.
Usage
import { PgReadonlyClient, QUERY_IDS } from 'readonly-postgres-mcp';
const pg = await PgReadonlyClient.fromEnv();
const result = await pg.readonly().run(QUERY_IDS.EXAMPLE_PING, {
params: { message: 'hello' },
});
await pg.readonly().query(
'SELECT table_name FROM information_schema.tables WHERE table_schema = $1 LIMIT 10',
{ values: ['public'] },
);
await pg.close();NestJS
import { PgReadonlyModule, NESTJS, PgReadonlyClient } from 'readonly-postgres-mcp/nestjs';
@Module({
imports: [PgReadonlyModule.forRoot()],
})
export class AppModule {}
@Injectable()
export class ReportService {
constructor(@Inject(NESTJS.PG_READONLY_CLIENT) private readonly pg: PgReadonlyClient) {}
}MCP server
Tool | Purpose | Shown when |
| Ad-hoc | Unless |
| List tables/views, or describe one relation's columns | Always |
| Named catalog query by | When |
All three are annotated readOnlyHint: true, so MCP clients that surface the
distinction show them as non-destructive.
pg_describe reads pg_catalog directly - faster than information_schema,
and it reports row estimates, comments and partitioned tables correctly. Let the
model call it rather than guessing at table names:
{} // list every relation in the search path
{ "table": "users" } // columns, types, nullability, defaults, primary keyspg-readonly-mcpAd-hoc example (pg_query_sql):
{
"sql": "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY 1 LIMIT 20"
}With positional params:
{
"sql": "SELECT table_name FROM information_schema.tables WHERE table_schema = $1 LIMIT 10",
"values": ["public"]
}Set PG_MCP_ALLOW_ADHOC=false to hide/disable pg_query_sql.
Limits applied to every query
Limit | MCP default | Setting |
Statement timeout | 15s |
|
Rows returned | 1,000 |
|
The row cap is enforced by PostgreSQL, not after the fact: statements are
wrapped as SELECT * FROM (<your query>) LIMIT <cap>+1, so a SELECT * against
a large table cannot exhaust the server's memory. Because the cap is pushed
down, rowCount reports rows returned, not rows matched, and truncated
tells you whether more exist.
Supported SQL
SELECT, WITH (non-data-modifying CTEs) and EXPLAIN. One statement per
call - no trailing second statement, and no semicolon needed.
EXPLAIN ANALYZE is rejected by default because it executes the statement
it explains. Set PG_ALLOW_EXPLAIN_ANALYZE=true if you need it. Plain EXPLAIN
always works.
Everything else is rejected before it reaches the database: INSERT, UPDATE,
DELETE, MERGE, COPY, CREATE, DROP, ALTER, TRUNCATE, GRANT,
REVOKE, VACUUM, REINDEX, CLUSTER, CALL, DO, SELECT INTO,
data-modifying CTEs, and multiple statements in one call.
Cursor MCP config:
{
"mcpServers": {
"readonly-postgres-mcp": {
"command": "npx",
"args": ["-y", "readonly-postgres-mcp"],
"env": {
"PG_HOST": "localhost",
"PG_DATABASE": "postgres",
"PG_USERNAME": "readonly_user",
"PG_PASSWORD": "...",
"PG_SSL_MODE": "require"
}
}
}
}Named query catalog (optional)
Instead of ad-hoc SQL, you can expose a fixed set of pre-approved queries.
Point PG_QUERY_REGISTRY at your own registry file; query paths resolve
relative to it, so a catalog is a self-contained folder:
my-catalog/
registry.json
queries/
reports/active-users.sqlWrite the
.sqlfile using:namedParamsRegister it in
registry.jsonwith its param typesSet
PG_QUERY_REGISTRY=/path/to/my-catalog/registry.jsonValidate with
npx pg-validate-catalog
Every catalog query is checked by the same SQL guard at startup, so a write statement in a catalog file stops the server rather than running.
Scripts
npm run validate:catalog
npm test
npm run build
npm run pack:checkPublishing to npm
npm login
npm run pack:check
npm publish --access publicDefense in depth
Layer | Mechanism |
SDK | SqlGuard allowlist + DML scan + param limits + no write API |
Connection |
|
Database | Readonly role with |
The database role is the security boundary; the other two layers are defense
in depth. The guard does not understand function calls, and a few functions
(dblink, nextval) escape a read-only transaction - see
SECURITY.md. Set the role up with
docs/db-role.sql, which includes a checklist for verifying
that writes actually fail.
Questions, ideas or feedback?
Email: amar141989@gmail.com
Or open a GitHub issue.
I read every email.
Available Tools
2 toolspg_describeARead-onlyIdempotent
Inspect the database schema.
Call with no arguments to list the tables, views and materialized views in the configured search path, with their estimated row counts and comments. Call with "table" to list that relation's columns, types, nullability, defaults, primary keys and comments.
Prefer this over querying information_schema by hand: it is faster and reports partitioned tables and comments correctly.
Inputs: table, schema.
pg_query_sqlARead-onlyIdempotent
Run a single read-only SQL query against a PostgreSQL database.
Only SELECT, WITH and EXPLAIN are permitted. Writes and DDL (INSERT, UPDATE, DELETE, MERGE, COPY, CREATE, DROP, ALTER, TRUNCATE) are rejected before reaching the database, as are data-modifying CTEs.
Exactly one statement per call — do not send two statements separated by a semicolon. A single trailing semicolon is fine.
Results are capped at 1000 rows and 15s. The response reports "truncated": true when more rows matched than were returned, and "rowCount" is the number of rows returned, not the number matched — use a COUNT(*) query if you need the true total.
Pass literals through "values" as $1, $2 placeholders rather than building them into the SQL string.
bigint and numeric columns are returned as JSON strings, not numbers.
Use pg_describe to discover tables and columns instead of guessing at names.
EXPLAIN ANALYZE is rejected by default because it executes the statement it explains; plain EXPLAIN always works.
Inputs: sql (required), values.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 2 tools
The two tools have completely distinct purposes: pg_query_sql executes read-only queries while pg_describe inspects schema metadata. There is no realistic scenario where an agent would confuse the two, and the descriptions even cross-reference each other (use pg_describe before pg_query_sql).
Both names use a consistent 'pg_' prefix and snake_case, which reads well as a set. The second segments differ structurally (query_sql is verb+format while describe is a bare verb), a minor deviation from a strict verb_noun pattern.
Two tools is minimal but justified for a deliberately read-only server whose entire surface is 'run a query' and 'inspect the schema'. It does not feel arbitrarily thin given the narrowly scoped purpose, though there is little room to grow.
Query execution plus schema discovery covers the core read-only lifecycle (explore schema, then query it). Minor gaps remain, such as no dedicated pagination/offset helper or multi-statement/session tooling, but row caps and the 'truncated' flag give agents workable paths.
Related MCP Connectors
Text-to-SQL MCP server: read-only queries on PostgreSQL, MySQL and SQL Server with your real schema
Hosted MCP server for PostgreSQL diagnostics: slow queries, missing indexes, connection pressure.
Managed Postgres MCP: projects, SQL, docs search, and storage buckets via OAuth.
Hosted PostgreSQL MCP server for Claude, ChatGPT and Cursor, with each action switched per account.
Related MCP Servers
- AlicenseBqualityDmaintenanceA lightweight Postgres MCP server for safe database exploration and query analysis, read-only by default, with multi-database support.44MIT
- AlicenseNot gradedqualityDmaintenanceRead-only PostgreSQL MCP server that enables running SELECT queries, listing tables and schemas, and describing columns, with built-in protection against writes and malicious SQL attacks.432 npmMIT
- AlicenseAqualityDmaintenanceA secure, read-only PostgreSQL MCP server that provides safe database introspection and querying capabilities.1420 npmMIT
- AlicenseNot gradedqualityDmaintenanceRead-only MCP server for PostgreSQL, enabling schema discovery, table metadata, and safe SELECT queries via READ ONLY transactions.12 npmMIT