Secure read-only PostgreSQL MCP server in Rust — a hardened alternative to the deprecated @modelcontextprotocol/server-postgres. AST-validated SELECT-only, timeouts, EXPLAIN cost guard, OAuth 2.1.
postgres-mcp-hardened demonstrates strong foundational quality with well-named tools, comprehensive descriptions, and proper JSON Schema definitions. All 6 tools are explicitly defined with input schemas and descriptions. Tool names follow verb-noun conventions (query, explain_query, list_schemas, list_tables, describe_table, security_posture). Descriptions are detailed (100-400+ chars) and include usage guidance, constraints, and warnings about pagination and data safety. However, several tools lack output schema documentation, and parameter descriptions could be more explicit about format constraints. Security posture is excellent (AST validation, write-blocking, timeouts) but not reflected in per-tool permission declarations. Error handling exists but lacks recovery guidance patterns.
Describe the columns of a table or view. Returns column names, types, nullability, and any COMMENT on the column. Also returns the primary key constraint and any foreign key constraints referencing other tables.
Explain the plan of a read-only query without running it. Returns the EXPLAIN output with cost estimates. Useful for understanding query performance and verifying that a query will not scan the entire table.
List all accessible schemas. Excludes pg_catalog, information_schema, and other system schemas. Respects the MCP_ALLOW_SCHEMAS configuration.
List all tables, views, materialized views, partitioned tables, and foreign tables in a schema. Returns the table name, type, and any COMMENT on the table. Respects the MCP_ALLOW_TABLES configuration to show only permitted tables.
Run a read-only SQL query and return rows. Writes, DDL and administrative functions are refused before the statement reaches the database. At most 1000 rows come back unless you pass `limit` (server maximum 10000); `truncated: true` in the response means there is more data — page through it with `offset`, and give the query an ORDER BY when you do, or the rows you get on page two depend on the planner's mood. This is for reading DATA. Do not hand-write catalog queries against pg_class or information_schema: `list_schemas`, `list_tables` and `describe_table` already return that, with comments and foreign keys, and they cannot be tripped up by search_path. For the plan of a statement use `explain_query` rather than writing EXPLAIN yourself.
Output schemas not documented in tool definitions. Tools return Values (Rust serde_json) but LLM cannot see expected response structure (fields, types, presence of pagination metadata, etc.). Without documented output schemas, agents must infer field names and cannot plan downstream calls reliably.
Parameter format constraints not explicitly stated in descriptions. 'sql' parameter accepts SELECT/WITH/VALUES/EXPLAIN/SHOW but LLM must infer this from prose description, not from schema enum. 'limit' and 'offset' lack explicit min/max bounds in descriptions (server internally caps at 10000 but LLM doesn't know this).
No tool-level permission declarations (read:database scope, write restrictions, etc.). Server implements write-blocking at AST level, but tools lack readOnlyHint or destructiveHint annotations to signal this to clients. Agents cannot infer safety properties.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | B | 72 | 2026-07-28+ | v2 |
Report on the security posture of this server: whether it is connected as a superuser, whether writes are blocked, whether statement timeouts are configured, and other hardening measures.
Error handling lacks recovery guidance. Code shows audit logging and error returns (e.g. err_content(-32000, msg)) but descriptions do not explain when errors are retryable, what the LLM should do next, or available alternatives. E.g. 'schema not found' errors lack suggestion to call list_schemas().
pagination not mentioned in tool descriptions despite query() supporting limit/offset. Description notes 'truncated: true' in response but does not explicitly state that pagination is required for result sets > 1000 rows. Agents may not realize they need to loop with offset.