pg-explain-mcp
Summary: This MCP server lets you analyze PostgreSQL query execution plans, inspect schema/indexes/parameters, and check server health.
explain: runEXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)onSELECT/WITHqueries and get a structuredplan_nodestree plus anissuesarray highlighting bottlenecks (e.g., seq scans, disk spills, nested loops, estimate mismatches, non-sargable predicates).list_tables: list all user tables and their columns.list_indexes: list existing indexes for a table or all tables (per README; not shown in schema).list_parameters: return runtime parameters relevant to plan analysis (work_mem,shared_buffers, etc.) (per README; not shown in schema).ping: health check returning'pong'.Configure enabled checks and thresholds dynamically via SQLite registry (
config/checks.db) without restarting the MCP server.
Provides tools for analyzing PostgreSQL query execution plans by running EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) on SELECT/WITH queries, returning structured reports on performance bottlenecks such as sequential scans, planner estimate mismatches, disk spills, and excessive nested loop iterations. Also lists user tables and their columns, with read-only database access.
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., "@pg-explain-mcpanalyze this query: SELECT * FROM orders WHERE status = 'pending'"
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.
pg-explain-mcp
An MCP (Model Context Protocol) server for analyzing PostgreSQL query execution plans. Built as a bridge between LLM-based coding assistants (Continue.dev, Claude Desktop, Cursor) and a PostgreSQL database.
The server runs EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) on SELECT
queries and returns a structured report highlighting performance
bottlenecks — so an LLM can explain why a query is slow and what to
do about it, instead of just describing the SQL.
Features
MCP tools
explain— runsEXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)on aSELECT/WITHquery and returns a structured report.list_tables— returns all user tables and their columns.list_indexes— returns existing indexes for a table (or all tables), so the assistant can avoid recommending an index that already exists.list_parameters— returns runtime parameters relevant to plan analysis:work_mem,hash_mem_multiplier,shared_buffers,effective_cache_size,random_page_cost,seq_page_cost, parallel worker limits,jit.ping— health check.
Checks
Every explain response includes an issues array. Each entry is
produced by an independent, pluggable PlanCheck:
Check | What it reports |
| Sequential scan that discards most of what it reads |
| Stale visibility map / poor heap locality |
| Large Bitmap Heap Scan |
| Sort spilling to disk ( |
| Hash operation using multiple batches |
| Nested Loop with a high number of inner iterations |
| Planner cardinality misestimate |
|
|
| Predicate wraps an indexed column in a function |
To add a new check, implement the PlanCheck protocol in
src/pg_explain_mcp/analyzer.py, register the class in
src/pg_explain_mcp/config.py, and add its default parameters to
config/seed.sql.
Structured plan output
explain returns a compact plan_nodes tree alongside issues.
Only the fields that matter for reasoning are kept:
node_type,relation,indexactual_rows,plan_rows,actual_loopsrows_removed_by_filter,heap_fetches,shared_read_blockssort_method,sort_space_type,sort_space_used_kbhash_buckets,hash_batches,peak_memory_usage_kbparallel_aware
The list of fields is configurable — see Configuration.
Related MCP server: Postgres Scout MCP
Installation
Requires Python 3.10+.
git clone https://github.com/Nick-Msk/pg-explain-mcp.git
cd pg-explain-mcp
python3.12 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install -e .pip install -e . installs the package in editable mode and registers
the pg-explain-mcp console script.
Verify
python -c "from pg_explain_mcp import server; print('OK')"
# → OK
pytest -vConfiguration
Connection
The server reads PostgreSQL connection parameters from environment variables:
Variable | Default | Description |
|
| PostgreSQL host |
|
| PostgreSQL port |
|
| Database user |
| — | Database password |
|
| Database name |
|
| Database (pg/orcl/mysql..) |
Check registry
Enabled checks and their thresholds live in a small SQLite database at
config/checks.db. The database is created automatically on first
explain if missing, and rebuilt from scratch by an explicit --init:
python -m pg_explain_mcp.config --init # rebuild from schema + seed
python -m pg_explain_mcp.config --show # print current stateChanges to the database take effect on the next explain call — no
MCP server restart required. For example:
# Disable NestedLoopCheck without touching the code.
sqlite3 config/checks.db \
"update checks set enabled = 0 where name = 'NestedLoopCheck';"
# Raise SeqScanCheck's threshold from 1000 to 5000 rows.
sqlite3 config/checks.db \
"update check_params set value = '5000'
where num = 1 and param = 'threshold_rows';"The schema is multi-database ready (checks and plan_fields are
keyed by database), but only postgres is currently populated. The
databases table is the FK root — removing a row from it cascades to
all of its checks, params, and plan fields.
To extend the registry with a new field for plan_nodes:
sqlite3 config/checks.db \
"insert into plan_fields (database, raw, key, enabled)
values ('postgres', 'Total Cost', 'total_cost', 1);"The new field appears in plan_nodes on the next explain call.
Usage with Continue.dev
Ready-to-use configuration files are available in
config_example/:
config_example/mcpServers/pg-explain.yaml— MCP server registration.config_example/postgres-agent.md— system prompt for an assistant that knows how to usepg-explainand a generic PostgreSQL MCP server, with per-check guidance.
Companion to universal-db-mcp
pg-explain-mcp is designed to be used alongside a
general-purpose PostgreSQL MCP server, not to replace it. In the
reference setup we use
universal-db-mcp
configured for the same database, and the two servers serve different
purposes:
Server | Purpose |
| General SQL execution: |
| Plan analysis: |
The assistant decides which to call based on the question:
"How many rows are in
orders?" →universal-db-mcp."Why is this query slow?" →
pg-explain-mcp.
All examples in usage_examples/ were captured with
both servers loaded. The agent configuration in
config_example/postgres-agent.md
describes both and includes guidance on when to prefer one over the
other.
Why not use pg-explain-mcp alone?
You can — the server is self-contained and exposes list_tables for
schema inspection. Disabling universal-db-mcp is perfectly fine if
you only care about plan analysis.
In practice, however, mixing the two is more convenient: pg-explain's
EXPLAIN ANALYZE actually executes every query, which is expensive
for large tables. For anything that does not need a plan — a row count,
a lookup, a quick sanity check — universal-db-mcp runs the same query
in read-only mode with much less overhead.
The split also reduces the blast radius of an LLM mistake: a bad
SELECT on universal-db-mcp is cheap, while a bad
EXPLAIN ANALYZE on pg-explain-mcp can be expensive. Keeping the
cheap path as the default makes the setup more robust.
Setup
Copy
config_example/mcpServers/pg-explain.yamlinto your workspace's.continue/mcpServers/directory.Replace the placeholders:
command:— full path to the Python interpreter inside your.venv.PG_USER,PG_PASSWORD,PG_DATABASE— your PostgreSQL credentials.
(Optional) Copy
config_example/postgres-agent.mdinto.continue/agents/to use it as a custom agent prompt.In VS Code:
Cmd+Shift+P→Continue: Reload Config.Open a new chat in Agent Mode (not Chat, not Edit).
Example
Prompt:
Use the pg-explain tool to analyze:
SELECT val FROM onek1 WHERE hundred BETWEEN 5 AND 55;
Response:
✓ Continue used the pg-explain explain tool
Execution Time: 0.904 ms
Planning Time: 1.228 ms
Issues Found: None
Analysis:
The plan shows an Index Only Scan on idx_onek1_hundred.
Heap Fetches: 0 — the visibility map is fresh, no heap lookups needed.Test fixtures
The repository ships a PostgreSQL extension
mcp_explain_tool that creates empty tables and
fill_* / clear_* procedures for every check. It is versioned
independently from the Python package.
cd fixtures
make installcreate extension mcp_explain_tool;
call mcp_explain_tool.fill_all(1000000);See fixtures/README.md for details, including
the VACUUM policy and the autovacuum exceptions for tables that need a
stale visibility map or fake statistics.
Usage examples
Real-world runs of pg-explain-mcp against PostgreSQL, one set per
check, with the raw tool output and analysis:
Check | Examples |
| healthy vs. stale visibility map |
| with and without an index |
| in-memory vs. external merge, plus a precise |
| single-batch vs. multi-batch spill, plus a precise variant |
| 100 vs. 5000 inner iterations |
| narrow vs. wide range on the same table |
| norm, norm+index, skewed, skewed-other-value |
| range predicate vs. non-sargable predicate on partitioned table |
| sargable vs. non-sargable predicate on indexed column |
See usage_examples/ for the full index, the test
environment, and prompting tips.
Project structure
pg-explain-mcp/
├── src/
│ └── pg_explain_mcp/
│ ├── __init__.py
│ ├── server.py # MCP entry point — exposes tools
│ ├── db.py # connection + EXPLAIN + schema queries
│ ├── analyzer.py # PlanCheck adapters + traversal
│ └── config.py # SQLite-backed check registry
├── config/ # SQLite config: schema, seed, checks.db
├── tests/ # unit tests for checks and helpers
├── fixtures/ # mcp_explain_tool PostgreSQL extension
├── usage_examples/ # real-world runs, one set per check
├── config_mcp/ # Continue.dev MCP + agent config
├── images/ # screenshots
├── pyproject.toml
├── CHANGELOG.md
├── DISCLAIMER.md
├── README.md
└── LICENSERoadmap
Additional checks
PartitionPruningCheck— detect queries on partitioned tables that failed to prune partitions. The plan showsAppend/Merge Appendwith a child count close to the total number of partitions, even when the predicate only matches one or two. Common in production, rarely covered by tutorials.NonSargableCheck— flag predicates wrapped in functions or casts (lower(email) = 'x',date_col::text = '2026-01-01') that prevent index usage. These show up asFilterentries instead ofIndex Cond. Fixable with a functional index or by rewriting the query.RepeatedScanCheck— report the same relation scanned more than once within a single plan (via CTEs, subqueries, or lateral joins, not self-joins). Often signals that CTE materialisation or a temp table would reduce I/O.jit_decision— flag expensive JIT compilation on short queries.
Database health checker
A second kind of check, alongside plan checks — health_check —
that inspects the database as a whole instead of a single query.
Health checks live in the same SQLite registry as plan checks,
distinguished by a new check_type column ('PLAN' or 'HEALTH').
This means they are configured, disabled, and tuned with the same
tools: show_params, set_checker_value, reset_checker_value,
and the TARGET_DB_TYPE scope all apply without change.
Candidate checks, each independently toggleable:
Tablespace free space. Warn when any tablespace has less than a configurable percentage of free space.
Invalid objects. Report indexes with
indisvalid = falseand constraints withconvalidated = false.plpgsql_checkintegration. If the extension is installed, runplpgsql_check_function()over every procedure and function in the target schema. If the extension is missing, skip with an INFO-level note.Bloat estimation. Compare
n_dead_tupton_live_tupinpg_stat_user_tablesand flag tables above a configurable ratio.Connection and lock pressure. Report long-running transactions and locks held beyond a configurable interval.
Implementation shape:
new
HealthCheckprotocol inanalyzer.py— sameIssuetype, samename/typeattributes, butcheck()takes no arguments and reads frompg_catalog/pg_stat_*directly;CheckRegistry.load_health()filtering bycheck_type = 'HEALTH';a single new MCP tool
health_checkreturning a report withchecks_appliedandissues, mirroring theexplainoutput shape.
Multi-database support
The PlanCheck interface and the SQLite config are database-agnostic
in principle. The next step is a MySQL/MariaDB adapter and its own
plan_fields / checks rows under TARGET_DB_TYPE=mysql.
Audit log for config changes
Record every set_checker_value and reset_checker_value call into
a param_history table with timestamp, old value, and new value.
Useful in shared deployments.
Disclaimer
This project is a diagnostic tool provided "as is". Recommendations
from the analyzer — or from an LLM assistant using it — are
suggestions, not guarantees. Always validate against your own database
before applying changes to a production system. EXPLAIN ANALYZE
actually executes the query; avoid running it against production
databases during peak hours.
See DISCLAIMER.md for the full text.
License
MIT — see LICENSE for details.
Available Tools
3 toolsexplainA
Run EXPLAIN ANALYZE on a SELECT query and return a structured report.
Args: sql: A SQL query. Only SELECT and WITH statements are allowed.
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries full responsibility for behavioral disclosure. It usefully notes that only SELECT and WITH statements are allowed, but it does not mention that EXPLAIN ANALYZE actually executes the query and may be resource-intensive, nor does it address error behavior. This is adequate but leaves notable behavioral traits implicit.
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 well-structured: a single front-loaded sentence states the action and output, followed by a minimal Args block. Every line earns its place with no filler or repetition.
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?
The description covers the core call requirements: the operation, the output, and the parameter constraint. The output schema supplies return structure. However, for a tool with no annotations, it should also mention that EXPLAIN ANALYZE executes the query, which affects cost and safety expectations, and provide clearer usage context.
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 0%, but the description compensates by defining sql as 'A SQL query' and adding the important SELECT/WITH restriction. For a single self-explanatory parameter, this is sufficient, though it omits minor details like whether semicolons or parameterized queries are accepted.
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 specific verb-resource pair ('Run EXPLAIN ANALYZE on a SELECT query') and states the deliverable ('structured report'). This makes the tool's purpose immediately clear and distinguishes it from sibling tools ping and list_tables without ambiguity.
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?
Usage context is implied: an agent would use this when it needs an EXPLAIN ANALYZE report for a SELECT/WITH query. However, the description does not explicitly state when to prefer this tool over alternatives or provide any exclusionary guidance beyond the allowed statement types.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_tablesA
Return a list of all user tables and their columns.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden. It discloses that the tool returns a list (read-only behavior) and scopes results to 'user tables' and 'their columns', which is meaningful behavioral context. However, it does not mention potential side effects, permissions, performance characteristics, or ordering, leaving some gaps for a simple read operation.
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?
A single, front-loaded sentence with zero filler. Every word contributes to the purpose, and it is appropriately sized for the tool's simplicity.
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 the tool has no parameters and an output schema exists, the description is fully sufficient. It tells the agent exactly what the tool returns, and no additional usage or behavioral information is required for correct invocation.
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, so the schema is empty and coverage is trivially 100%. Per the rubric, 0 parameters earns a baseline of 4; the description adds nothing about parameters because there are none, and none are needed.
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 ('Return') and resource ('list of all user tables and their columns'), clearly distinguishing it from siblings 'ping' and 'explain'. It states exactly the operation and scope without ambiguity.
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 provides no guidance on when to use this tool versus alternatives, no prerequisites, and no exclusions. It only states what the tool does, leaving the agent to infer usage context from the tool name and sibling names.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
pingA
Health check — returns 'pong' if the server is running.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden of behavioral disclosure. It discloses the success behavior (returns 'pong') and implicitly signals a non-mutating operation, but it does not state what happens on failure — whether the call errors, times out, or returns a non-pong payload when the server is down.
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?
A single front-loaded sentence that wastes no words: the purpose ('Health check') appears first, and the expected response is stated immediately. Every word 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 zero-parameter tool with an output schema present, the description covers the essential purpose and response behavior. It could add failure semantics, but the output schema handles return-value details and the tool's simplicity means nothing critical 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, so the description correctly omits parameter details and the empty schema is fully covered. With no parameters to document, the baseline of 4 applies.
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 clear, specific purpose: a liveness health check that returns 'pong' when the server is running. This unambiguously distinguishes it from siblings list_tables and explain — there is no overlap in function, and an agent can tell immediately what this tool does.
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 usage context is implied but not explicit: an agent would infer this is the tool to call when verifying server liveness, and the sibling tools are clearly unrelated. However, there is no explicit statement of when to use it vs. alternatives, no prerequisites, and no named exclusions.
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.
3 tool updates
v0.1.0- First observed
explain - First observed
list_tables - First observed
ping
TDQS
Scored across 3 tools
Each tool has a unique role: ping is a health check, list_tables surfaces schema metadata, and explain performs query analysis. There is no overlap or ambiguity in selecting among them.
Names are short, lowercase verb forms and are easy to predict. list_tables follows the verb_noun pattern, while ping and explain are bare verbs, so the pattern is not perfectly uniform.
Three tools is minimal but appropriate for the narrow purpose of examining table schemas and running EXPLAIN ANALYZE. Each tool serves a distinct, necessary function without redundancy.
Within its stated scope of listing tables and explaining SELECT/WITH queries, the workflow is complete: discover tables, write a query, and get a structured plan. The restriction to read-only queries is intentional and avoids unsafe mutations.
Maintenance
Related MCP Connectors
Query PostgreSQL databases in plain English — LLM-generated, safety-validated SQL.
Generate, fix, explain and run read-only SQL on PostgreSQL, MySQL and SQL Server
Deterministic safety, correctness & cost gate that vets Postgres SQL before your AI agent runs it.
Safe, read-only Postgres and MySQL access for AI agents. Audit log + column-level controls.
Related MCP Servers
- FlicenseAqualityDmaintenanceEnables AI assistants to interact with PostgreSQL databases using natural language queries, providing secure read-only access to database schemas and SQL translation capabilities.611 npm-
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to safely explore, analyze, and maintain PostgreSQL databases with read-only mode by default, SQL injection prevention, query performance analysis, and optional write operations.37 npmApache 2.0
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to safely interact with PostgreSQL databases, perform queries, inspect schemas, and analyze query performance.2-
- AlicenseNot gradedqualityCmaintenanceSecurely connect AI assistants to PostgreSQL databases with read-only access, schema discovery, querying, and performance analysis tools.6 npmMIT