MCP PostgreSQL Server
This server lets an MCP client (e.g. Claude Code, VS Code, Cursor) interact with a PostgreSQL database through read-only queries and optional writes.
Run read-only SQL queries with
$1-style parameters, returning compact JSON with truncation when results are largeList schemas, list tables in a schema, and describe table structure
Execute INSERT/UPDATE/DELETE/DDL statements when
PG_ALLOW_WRITE=true(writes refused by default)Switch to another database at runtime via
connect_dbwhenPG_ENABLE_RUNTIME_CONNECT=trueConnect to local, Docker, RDS, Neon, Supabase, and SSH-tunneled PostgreSQL databases
Enforce read-only mode using
BEGIN READ ONLYtransactions, not client-side SQL parsingConfigure connection via
DATABASE_URLor individualPG_*variables, with SSL and timeoutsReturn SQL errors with SQLSTATE codes and hints so the model can self-correct
Supports installation and execution through npm and npx commands, allowing for easy deployment and integration of the MCP server within Node.js environments.
Enables interaction with PostgreSQL databases, providing tools for executing queries, managing database connections, listing tables, and describing table structures with support for prepared statements and comprehensive error handling.
Offers TypeScript support for type-safe interactions with PostgreSQL databases through the MCP server interface.
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., "@MCP PostgreSQL Servershow me the 10 most recent orders from the sales table"
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.
MCP PostgreSQL Server
A Model Context Protocol (MCP) server for PostgreSQL: local, Docker, RDS, Neon, and Supabase databases.
The server is small and auditable, with four runtime dependencies: the MCP SDK,
pg, pg-connection-string, and zod (plus ssh2, an optional dependency used
only for SSH tunneling).
Requires Node.js 20 or newer.
Quick start
The preferred way to configure the server is a single DATABASE_URL:
{
"mcpServers": {
"postgres": {
"type": "stdio",
"command": "npx",
"args": ["-y", "mcp-postgres-server"],
"env": {
"DATABASE_URL": "postgres://user:password@localhost:5432/mydb",
"PG_ALLOW_WRITE": "false"
}
}
}
}With PG_ALLOW_WRITE set to "false" the server has read-only access to the
database. This is the default; set it to "true" only if the model must write.
The same JSON works in any MCP client that speaks stdio: VS Code, Cursor, Claude Code, Codex, Windsurf.
Alternatively, set the individual PG_* variables; they are used when
DATABASE_URL is not set:
{
"mcpServers": {
"postgres": {
"type": "stdio",
"command": "npx",
"args": ["-y", "mcp-postgres-server"],
"env": {
"PG_HOST": "your_host",
"PG_PORT": "5432",
"PG_USER": "your_user",
"PG_PASSWORD": "your_password",
"PG_DATABASE": "your_database",
"PG_ALLOW_WRITE": "false"
}
}
}
}Manual Installation
npm install mcp-postgres-serverOr run directly with:
npx mcp-postgres-serverRelated MCP server: PostgreSQL MCP Server
Connect to your database
Local Postgres:
DATABASE_URL=postgres://mcp_readonly:secret@localhost:5432/mydbPostgres in Docker: if the database runs in a container with a published
port, connect to localhost:<published-port> as usual. If the MCP server
itself runs inside a container and the database runs on your host machine,
use host.docker.internal instead of localhost:
DATABASE_URL=postgres://mcp_readonly:secret@host.docker.internal:5432/mydbAmazon RDS:
DATABASE_URL=postgres://mcp_readonly:secret@mydb.xxxxxx.us-east-1.rds.amazonaws.com:5432/mydb?sslmode=requireNeon:
DATABASE_URL=postgres://mcp_readonly:secret@ep-xxx-xxx.us-east-2.aws.neon.tech/mydb?sslmode=requireSupabase:
DATABASE_URL=postgres://postgres.xxxxxxxx:secret@aws-0-us-east-1.pooler.supabase.com:5432/postgres?sslmode=requireTools
Tool availability depends on configuration:
Tool | Available |
| Always |
| Always (refuses writes unless |
| Only when |
1. query
Execute a read-only SQL statement. Accepts SELECT, WITH ... SELECT,
EXPLAIN, and SHOW. One statement per call - multi-statement input is rejected
by the extended query protocol. In read-only mode (the default) the statement runs
as BEGIN READ ONLY, the query, and ROLLBACK - three commands, roughly two
network round trips with pipelining - so the database itself refuses any write.
With PG_ALLOW_WRITE=true the statement is sent directly, without that wrapper, so a
write run through query would execute - use execute for writes.
Supports PostgreSQL-style $1, $2 prepared-statement parameters; values are bound
by the driver and never spliced into the SQL text.
use_mcp_tool({
server_name: "postgres",
tool_name: "query",
arguments: {
sql: "SELECT * FROM users WHERE id = $1",
params: [1]
}
});Returns compact JSON: {"rows": [...], "rowCount": n, "returnedRows": n, "truncated": false}.
When the serialized rows exceed PG_MAX_RESULT_BYTES, only the rows that fit are returned
(returnedRows < rowCount), truncated is true, and a hint suggests adding LIMIT/WHERE
or selecting fewer columns.
2. list_schemas
List all schemas in the connected database.
use_mcp_tool({
server_name: "postgres",
tool_name: "list_schemas",
arguments: {}
});3. list_tables
List tables in the connected database. Accepts an optional schema parameter (defaults to 'public').
// List tables in the 'public' schema (default)
use_mcp_tool({
server_name: "postgres",
tool_name: "list_tables",
arguments: {}
});
// List tables in a specific schema
use_mcp_tool({
server_name: "postgres",
tool_name: "list_tables",
arguments: {
schema: "my_schema"
}
});4. describe_table
Get the structure of a specific table (columns, types, nullability, defaults, primary keys). Accepts an optional schema parameter (defaults to 'public').
use_mcp_tool({
server_name: "postgres",
tool_name: "describe_table",
arguments: {
table: "users",
schema: "my_schema" // optional
}
});5. execute - requires PG_ALLOW_WRITE=true
Execute an INSERT, UPDATE, DELETE, or DDL statement. Always registered, but
in read-only mode (the default) it refuses with an error naming PG_ALLOW_WRITE
and changes nothing - the statement never reaches the database. With
PG_ALLOW_WRITE=true it runs: same $1, $2 parameter handling as query, one
complete statement per call, and the connecting role governs what it may do.
Returns {"rowCount": n, "command": "INSERT"}.
use_mcp_tool({
server_name: "postgres",
tool_name: "execute",
arguments: {
sql: "INSERT INTO users (name, email) VALUES ($1, $2)",
params: ["John Doe", "john@example.com"]
}
});6. connect_db - requires PG_ENABLE_RUNTIME_CONNECT=true
Connect to a different PostgreSQL database at runtime using provided
credentials. Not registered by default - prefer configuring credentials
through the environment so they never pass through model-visible arguments.
Session limits (statement_timeout, idle_in_transaction_session_timeout) are
re-applied after every reconnect; read-only reads enforce read-only in their own
BEGIN READ ONLY transaction.
use_mcp_tool({
server_name: "postgres",
tool_name: "connect_db",
arguments: {
host: "localhost",
port: 5432,
user: "your_user",
password: "your_password",
database: "your_database"
}
});Configuration reference
Variable | Default | Description |
| - | Full connection string (preferred). Supports |
| - | Database host (fallback when |
|
| Database port |
| - | Database user |
| - | Database password |
| - | Database name |
|
| When |
| - |
|
| - | Path to a CA certificate file. Setting it by itself implies |
|
| Register the |
|
| Byte budget for a |
|
| Statement timeout in milliseconds, applied to every session |
|
| Timeout in milliseconds for a single connect attempt (raise it for slow links or SSH tunnels) |
To reach a database only accessible through a bastion, see SSH tunneling (adds PG_SSH_* variables).
Features
Read-only by default; writes are an explicit opt-in (
PG_ALLOW_WRITE=true)Read-only enforced by the engine (
BEGIN READ ONLY), never by client-side SQL parsingData access behind a small typed interface; the
pgdriver never leaks past itDATABASE_URLsupport with SSL (sslmode=disable|allow|prefer|require|verify-ca|verify-full, custom CA)Prepared-statement parameters:
$1-style placeholders, bound by the driverResult size cap (byte budget) with an explicit
truncatedflag instead of flooding the model's contextSession statement timeout plus a client deadline; transaction poolers may not preserve session settings
Errors returned as readable tool results with SQLSTATE-based hints, so the model can self-correct
Survives dropped connections - reconnects lazily instead of crashing
Optional SSH tunneling (
PG_SSH_*) with mandatory host-key verification, loaded only when configuredMCP tool annotations (read-only / destructive hints) per spec 2025-11-25
Multi-schema support for database operations
Security
Full details, including the threat model and disclosure process, are in SECURITY.md. The short version:
A least-privilege database role is the real boundary. The MCP works with existing credentials; creating or changing roles is not required. A dedicated role is what actually guarantees writes are impossible. On PostgreSQL 14+, the following is a starting point:
CREATE ROLE mcp_readonly LOGIN PASSWORD 'change-me'; GRANT CONNECT ON DATABASE your_database TO mcp_readonly; GRANT pg_read_all_data TO mcp_readonly; -- adds read privileges ALTER ROLE mcp_readonly SET default_transaction_read_only = on; -- read-only by default(On PostgreSQL 13 or older, grant
SELECTexplicitly instead ofpg_read_all_data- see SECURITY.md.) The server warns on stderr if you connect as a superuser. Read grants do not revoke existing privileges, and defaults remain mutable; available functions, ownership and inherited privileges also matter.The engine enforces read-only. There is no client-side SQL parsing. In read-only mode every read runs in a rolled-back
BEGIN READ ONLYtransaction, so PostgreSQL itself - which alone knows what a function, view, or rule does - refuses any write with SQLSTATE 25006 and reverts any session change the statement made. The extended protocol rejects multi-command strings.
Honest framing: the read-only transaction is defense-in-depth on top of the role, not a replacement for it. Read-only mode stops a confused or prompt-injected model from writing to your database; it does not stop prompt injection carried in the row data a query returns. Don't point this server at production - use a replica, a snapshot, or a tightly scoped role. See SECURITY.md.
SSH tunneling
Set PG_SSH_HOST (plus auth and host-key verification) to reach a database that is only accessible
through a bastion (an SSH jump host). The connection string / PG_* fields then describe the
database as seen from the bastion:
{
"mcpServers": {
"postgres": {
"type": "stdio",
"command": "npx",
"args": ["-y", "mcp-postgres-server"],
"env": {
"DATABASE_URL": "postgres://mcp_readonly:secret@db.internal:5432/mydb?sslmode=verify-full",
"PG_SSH_HOST": "bastion.example.com",
"PG_SSH_USER": "jump",
"PG_SSH_PRIVATE_KEY": "/home/me/.ssh/id_ed25519",
"PG_SSH_FINGERPRINT": "SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}
}Variable | Default | Description |
| - | SSH bastion host. Setting it enables tunneling: the server reaches the database only through an SSH tunnel to this host (see below). Optional feature; needs the |
|
| SSH bastion port |
| - | SSH username |
| - | Path to a private key file. If unset, auth falls back like |
| - | Passphrase for the private key, if encrypted |
| - |
|
| - | SSH login password. Opt-in; a key or agent takes precedence. Prefer keys - a bastion often disables password auth. |
| - | Pinned host-key fingerprint ( |
|
| SSH keepalive interval in ms; the tunnel drops after 3 unanswered keepalives, and the next call reconnects |
SSH changes only the transport. Read-only enforcement, the result size cap, timeouts, and
connect_dbbehave exactly as on a direct connection, and no extra SQL is sent per query.Host-key verification is mandatory via a pinned
PG_SSH_FINGERPRINT- the tunnel will not connect without it, so a man-in-the-middle bastion is refused. Get the fingerprint over a channel you trust, most trustworthy first:on the bastion itself, or from its admin:
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub(no network involved);from your existing
~/.ssh/known_hosts, if you already reach the host overssh:ssh-keygen -lF bastion.example.com;fetched from the host:
ssh-keyscan bastion.example.com | ssh-keygen -lf -(trust this only when run from a network position you trust - it accepts whatever the host returns).
TLS validates the real database hostname. With
verify-full, the certificate is checked against the database's own hostname (e.g.db.internal), not the loopback the tunnel binds locally, andrejectUnauthorizedis pinned on so an inheritedNODE_TLS_REJECT_UNAUTHORIZED=0cannot disable it.ssh2is an optional dependency, loaded only whenPG_SSH_HOSTis set, so a direct connection never initializes it. npm installs optional dependencies by default; runnpm install --omit=optionalto skip it entirely (a direct connection does not need it).
A tunneled connection that fails reports a stable SSH_* code - see Error Handling.
Error Handling
SQL and connection failures are returned as tool results (isError: true)
with a message, the SQLSTATE code, and a hint. PostgreSQL's own server hint is
used when present; otherwise these fallbacks apply:
code | meaning | first thing to check |
| authentication failed |
|
| database does not exist |
|
| relation not found | call |
| column not found | call |
| the query was canceled (a timeout or a cancel request) | if timing out, add a |
| the transaction is read-only | source may be a read-only role, a replica, a server default, or (for |
| cannot reach or resolve the database host |
|
Over an SSH tunnel, a failure carries a stable code (and, where
the cause is determinate, a hint naming the setting to fix), so the failing phase is unambiguous:
code | meaning | first thing to check |
| invalid SSH config, incl. a malformed | the |
| key unreadable, unparseable, a public key, or encrypted without the right passphrase (an encrypted key with the correct |
|
| the bastion is unreachable, or SSH setup failed for an unclassified reason |
|
| the bastion did not respond in time | network/firewall, |
| the bastion rejected authentication |
|
| host key does not match | re-fetch the fingerprint |
| tunnel is up, but the bastion could not reach the database | the DB host and port as seen from the bastion |
| an established tunnel dropped mid-session | transient; the next call reconnects |
A genuine PostgreSQL error through a healthy tunnel keeps its own code (e.g. 28P01 for wrong
database credentials), not an SSH code.
Migrating from 0.1.x
Not needed for new installs. Two behavior changes since 0.1.x:
Read-only by default. The
executetool is always visible but refuses writes (with an error naming the flag) unlessPG_ALLOW_WRITE=true, and every read runs inside an engine-enforcedREAD ONLYtransaction. If your workflow writes to the database, set"PG_ALLOW_WRITE": "true"to restore 0.1.x behavior.connect_dbis disabled by default. Runtime connection switching (passing credentials through tool arguments) requiresPG_ENABLE_RUNTIME_CONNECT=true; otherwise connection details come only from the environment.
Tool names, parameter names, and PG_* variables are unchanged. Result payloads
are now structured compact JSON for every tool (e.g. query returns
{rows, rowCount, returnedRows, truncated} instead of a bare row array) - see
CHANGELOG.md for the exact shapes before updating anything that
parses tool output.
License
MIT
Available Tools
5 toolsdescribe_tableDescribe a tableARead-only
Show the structure of one table: column names, data types, nullability, defaults, and primary-key membership. Call this before writing non-trivial queries against a table. Returns {columns: [{column, type, nullable, default, is_primary_key}, ...]}.
| Name | Required | Description | Default |
|---|---|---|---|
| table | Yes | Table name | |
| schema | No | Schema name (default: 'public') |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The readOnlyHint annotation already covers non-mutation, and the description adds concrete behavioral detail beyond it by specifying the exact result shape and fields. It does not go into error behavior or edge cases, but given the annotation coverage, the added precision about return contents earns a solid score.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two sentences plus a compact inline return-shape definition. It front-loads the core purpose, adds a usage cue, and then gives the exact response structure without any filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple 2-parameter read-only introspection tool, the description is complete: it states what the tool does, when to use it, what it returns, and the annotations cover safety. No output schema exists, but the inline return format compensates for that.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%: both 'table' and 'schema' are documented in the schema. The description adds no parameter-specific meaning beyond what the schema already provides, so the baseline of 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource: 'Show the structure of one table', and enumerates exactly what is returned (column names, data types, nullability, defaults, primary-key membership). This clearly distinguishes it from siblings like query/execute (run statements) and list_tables/list_schemas (enumerate catalog objects).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives explicit guidance: 'Call this before writing non-trivial queries against a table.' This is clear contextual advice, though it does not name alternatives or state when not to use the tool. It stops short of the explicit when/when-not/alternatives structure that would earn a 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
executeRun a write statement (disabled)ADestructive
Run a data-modifying SQL statement (INSERT/UPDATE/DELETE or DDL). Currently DISABLED: the server is read-only, so this returns an error and changes nothing. To enable writes, the operator must start the server with PG_ALLOW_WRITE=true.
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | One INSERT / UPDATE / DELETE / DDL statement. Use $1, $2, ... for parameters. | |
| params | No | Positional parameter values bound to $1, $2, ... placeholders. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Adds critical context beyond annotations: it returns an error and changes nothing due to the server being read-only, and explains how to enable writes. This is transparent and consistent with destructiveHint=true and readOnlyHint=false—no contradiction.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two concise sentences that front-load the purpose, then immediately disclose the disabled state and the fix. No wasted words, and the structure is logical.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a disabled write tool with no output schema, the description fully explains current behavior and the enabling condition. It doesn't describe success response when enabled, but that's irrelevant given the disabled state; the agent knows calling it will fail now.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description covers both parameters fully (sql statement and positional params), including the placeholder syntax. The description restates the statement type but adds no new parameter semantics beyond what the schema already provides, so baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description explicitly states it runs data-modifying SQL (INSERT/UPDATE/DELETE/DDL), which distinguishes it from sibling tools like query and describe_table. It also indicates the tool is currently disabled, adding precise scope.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It clearly says the tool is disabled, explains the server read-only condition, and gives the exact flag needed to enable it (PG_ALLOW_WRITE=true). This provides strong when-not-to-use guidance, though it doesn't explicitly name alternatives like query for read operations.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_schemasList schemasARead-only
List every schema in the connected database. Start here when exploring an unfamiliar database, then call list_tables for the schema you care about. Returns {schemas: [name, ...]}.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, so the safe-read nature is covered. The description adds useful behavioral context by specifying 'every schema in the connected database' and providing the return shape, which is valuable since there is no output schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two concise sentences with no filler. The core action is front-loaded, followed by usage guidance and a compact return-format note.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-parameter, read-only discoverability tool, the description is complete: it states the operation, the scope, the recommended next step, and the return shape. Nothing an agent needs to call it successfully is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters and an empty input schema, so there is nothing to explain. Baseline for a zero-parameter tool is 4; the description correctly omits irrelevant parameter details.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource: 'List every schema in the connected database.' The scope is clear and it differentiates itself from sibling tools by framing itself as the entry point before list_tables.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly directs the agent when to use the tool: 'Start here when exploring an unfamiliar database.' It also names the next step, list_tables, giving clear usage sequencing among siblings.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_tablesList tablesARead-only
List all tables in a schema (default: 'public'). Use this before querying tables you have not seen yet, then call describe_table for column details. Returns {tables: [name, ...]}.
| Name | Required | Description | Default |
|---|---|---|---|
| schema | No | Schema name (default: 'public') |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The annotations already declare readOnlyHint=true, so the read-only nature is established. The description adds useful behavioral context beyond that: the default schema behavior and the exact return shape {tables: [name, ...]}, which is valuable because there is no output schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three sentences with no wasted words: the first states purpose and default, the second gives usage guidance, and the third documents the return format. It is front-loaded and every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple list tool with one optional parameter and annotations covering safety, the description is complete. It tells the agent what the tool does, when to use it, what the default is, and what the response looks like, so nothing essential is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, and the schema already documents the single optional schema parameter with its default value. The description repeats the default-public behavior, so it adds no meaning beyond what the input schema provides.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb and resource: 'List all tables in a schema (default: 'public')'. It also distinguishes itself from the sibling describe_table by noting that describe_table should be used afterward for column details, making the tool's role clear.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly says to use the tool before querying tables not yet seen, and directs the agent to call describe_table next for column details. This provides clear when-to-use guidance and names the relevant alternative.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
queryRun read-only SQLARead-only
Run one read-only SQL statement against the connected PostgreSQL database and get rows back as JSON. Send exactly one statement per call (SELECT, WITH, EXPLAIN, or SHOW). It runs inside an engine-enforced read-only transaction, so any write is refused by the database. Use this tool for all data reading, aggregation, and query planning. Returns {rows, rowCount, returnedRows, truncated}, plus hint when truncated is true. Prefer $1, $2 placeholders with the params array over interpolating values. Results are capped at ~32768 bytes; truncated:true means rows were dropped - add LIMIT/WHERE or select fewer columns.
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | One SQL statement. Use $1, $2, ... for parameters. | |
| params | No | Positional parameter values bound to $1, $2, ... placeholders. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the readOnlyHint annotation, the description adds meaningful behavioral detail: the engine-enforced read-only transaction, the exact return shape {rows, rowCount, returnedRows, truncated}, the ~32768 byte cap with truncated:true behavior, and concrete remediation advice (add LIMIT/WHERE or fewer columns). This goes far beyond what annotations alone provide.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is compact and every sentence earns its place: purpose, statement constraint, enforcement, usage scope, return shape, binding guidance, and truncation handling. The most important information is front-loaded in the first sentence, and there is no filler or repetition of schema content.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given there is no output schema, the description fully compensates by detailing the return fields and truncation behavior. It also covers statement type restrictions, read-only enforcement, result size cap, and safe parameter binding. An agent has everything needed to invoke this tool correctly without additional inference.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
With 100% schema description coverage, the schema already documents both parameters. The description adds value above the baseline by instructing agents to 'prefer $1, $2 placeholders with the params array over interpolating values,' a safety/security nuance not present in the schema, and by enforcing 'exactly one statement per call.'
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a precise verb+resource: 'Run one read-only SQL statement against the connected PostgreSQL database and get rows back as JSON.' It further specifies allowed statement types (SELECT, WITH, EXPLAIN, SHOW), which clearly separates it from siblings like list_tables or execute.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly states when to use the tool: 'Use this tool for all data reading, aggregation, and query planning.' The 'read-only' framing and 'any write is refused' communicate the boundary against writes, though it doesn't explicitly name the write sibling (execute) as the alternative, so it falls just short of full 5.
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.
6 tool updates
v0.3.0- Removed
connect_db - Changed
describe_table3 fields changed- added
Input schema / $schemaAdded value: +"http://json-schema.org/draft-07/schema#" - added
Input schema / additionalPropertiesAdded value: +false - changed
Input schema / properties / schema / descriptionPrevious value: -"Schema name (default: public)"New value: +"Schema name (default: 'public')"
- Changed
execute4 fields changed- added
Input schema / $schemaAdded value: +"http://json-schema.org/draft-07/schema#" - added
Input schema / additionalPropertiesAdded value: +false - changed
Input schema / properties / params / descriptionPrevious value: -"Query parameters (optional)"New value: +"Positional parameter values bound to $1, $2, ... placeholders." - changed
Input schema / properties / sql / descriptionPrevious value: -"SQL query (INSERT, UPDATE, DELETE) (use $1, $2, etc. for parameters)"New value: +"One INSERT / UPDATE / DELETE / DDL statement. Use $1, $2, ... for parameters."
- Changed
list_schemas2 fields changed- added
Input schema / $schemaAdded value: +"http://json-schema.org/draft-07/schema#" - removed
Input schema / requiredRemoved value: -[]
- Changed
list_tables4 fields changed- added
Input schema / $schemaAdded value: +"http://json-schema.org/draft-07/schema#" - added
Input schema / additionalPropertiesAdded value: +false - changed
Input schema / properties / schema / descriptionPrevious value: -"Schema name (default: public)"New value: +"Schema name (default: 'public')" - removed
Input schema / requiredRemoved value: -[]
- Changed
query4 fields changed- added
Input schema / $schemaAdded value: +"http://json-schema.org/draft-07/schema#" - added
Input schema / additionalPropertiesAdded value: +false - changed
Input schema / properties / params / descriptionPrevious value: -"Query parameters (optional)"New value: +"Positional parameter values bound to $1, $2, ... placeholders." - changed
Input schema / properties / sql / descriptionPrevious value: -"SQL SELECT query (use $1, $2, etc. for parameters)"New value: +"One SQL statement. Use $1, $2, ... for parameters."
6 tool updates
- First observed
connect_db - First observed
describe_table - First observed
execute - First observed
list_schemas - First observed
list_tables - First observed
query
TDQS
Scored across 5 tools
Each tool targets a clearly separate concern: read queries, write execution, schema discovery, table discovery, and column metadata. There is no meaningful overlap, and the descriptions reinforce the boundaries.
All tool names follow a clean verb_object pattern in snake_case: query, execute, list_schemas, list_tables, describe_table. The naming convention is consistent and predictable.
Five tools is well-scoped for a PostgreSQL server: one read path, one write path, and three introspection tools for exploring the database structure. No tool feels redundant or missing.
The tool surface covers the core database workflow well: discover schemas, inspect tables, describe columns, run read queries, and execute writes. The only caveat is that execute is disabled by default, so write workflows are not available unless the operator explicitly enables them.
Maintenance
Related MCP Connectors
Query PostgreSQL databases in plain English — LLM-generated, safety-validated SQL.
- dataOAuthco.thinair
PostgreSQL, MySQL, and SQL Server in one session. 26 read-only MCP tools for AI agents.
Comprehensive PostgreSQL documentation and best practices, including ecosystem tools
Deterministic safety, correctness & cost gate that vets Postgres SQL before your AI agent runs it.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables LLMs to interact deeply with PostgreSQL databases—query data, manage schema, analyze performance, and administer the database.152 npm4MIT
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to interact with PostgreSQL databases through natural language queries, schema inspection, and safe SQL execution.7 npm1-
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to interact with PostgreSQL databases through the Model Context Protocol, supporting SQL queries, schema management, and data operations.7 npmMIT
- FlicenseNot gradedqualityCmaintenanceEnables AI assistants to safely interact with PostgreSQL databases, perform queries, inspect schemas, and analyze query performance.2-