Skip to main content
Glama
Nick-Msk
by Nick-Msk

pg-explain-mcp

Tag Python License MCP SQL lint

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 — runs EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) on a SELECT / WITH query 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

SeqScanCheck

Sequential scan that discards most of what it reads

IndexScanCheck

Stale visibility map / poor heap locality

BitmapHeapScanCheck

Large Bitmap Heap Scan

DiskSpillSortCheck

Sort spilling to disk (external merge)

DiskSpillHashCheck

Hash operation using multiple batches

NestedLoopCheck

Nested Loop with a high number of inner iterations

EstimateMismatchCheck

Planner cardinality misestimate

PartitionPruningCheck

Append over many partitions — pruning may have failed

NonSargableCheck

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, index

  • actual_rows, plan_rows, actual_loops

  • rows_removed_by_filter, heap_fetches, shared_read_blocks

  • sort_method, sort_space_type, sort_space_used_kb

  • hash_buckets, hash_batches, peak_memory_usage_kb

  • parallel_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 -v

Configuration

Connection

The server reads PostgreSQL connection parameters from environment variables:

Variable

Default

Description

PG_HOST

localhost

PostgreSQL host

PG_PORT

5432

PostgreSQL port

PG_USER

postgres

Database user

PG_PASSWORD

—

Database password

PG_DATABASE

postgres

Database name

TARGET_DB_TYPE

postgres

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 state

Changes 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/:

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

universal-db-mcp

General SQL execution: SELECT, schema exploration, ad-hoc queries.

pg-explain-mcp

Plan analysis: EXPLAIN ANALYZE, structured bottleneck detection, index/parameter inspection.

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

  1. Copy config_example/mcpServers/pg-explain.yaml into your workspace's .continue/mcpServers/ directory.

  2. Replace the placeholders:

    • command: — full path to the Python interpreter inside your .venv.

    • PG_USER, PG_PASSWORD, PG_DATABASE — your PostgreSQL credentials.

  3. (Optional) Copy config_example/postgres-agent.md into .continue/agents/ to use it as a custom agent prompt.

  4. In VS Code: Cmd+Shift+P → Continue: Reload Config.

  5. 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 install
create 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

IndexScanCheck

healthy vs. stale visibility map

SeqScanCheck

with and without an index

DiskSpillSortCheck

in-memory vs. external merge, plus a precise work_mem variant

DiskSpillHashCheck

single-batch vs. multi-batch spill, plus a precise variant

NestedLoopCheck

100 vs. 5000 inner iterations

BitmapHeapScanCheck

narrow vs. wide range on the same table

EstimateMismatchCheck

norm, norm+index, skewed, skewed-other-value

PartitionPruningCheck

range predicate vs. non-sargable predicate on partitioned table

NonSargableCheck

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
└── LICENSE

Roadmap

Additional checks

  • PartitionPruningCheck — detect queries on partitioned tables that failed to prune partitions. The plan shows Append / Merge Append with 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 as Filter entries instead of Index 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:

  1. Tablespace free space. Warn when any tablespace has less than a configurable percentage of free space.

  2. Invalid objects. Report indexes with indisvalid = false and constraints with convalidated = false.

  3. plpgsql_check integration. If the extension is installed, run plpgsql_check_function() over every procedure and function in the target schema. If the extension is missing, skip with an INFO-level note.

  4. Bloat estimation. Compare n_dead_tup to n_live_tup in pg_stat_user_tables and flag tables above a configurable ratio.

  5. Connection and lock pressure. Report long-running transactions and locks held beyond a configurable interval.

Implementation shape:

  • new HealthCheck protocol in analyzer.py — same Issue type, same name / type attributes, but check() takes no arguments and reads from pg_catalog / pg_stat_* directly;

  • CheckRegistry.load_health() filtering by check_type = 'HEALTH';

  • a single new MCP tool health_check returning a report with checks_applied and issues, mirroring the explain output 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 tools
explainA

Run EXPLAIN ANALYZE on a SELECT query and return a structured report.

Args: sql: A SQL query. Only SELECT and WITH statements are allowed.

ParametersJSON Schema
NameRequiredDescriptionDefault
sqlYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A3.9/5.0
Behavior3/5

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.

Conciseness5/5

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.

Completeness3/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines3/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A3.9/5.0
Behavior3/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines2/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4/5.0
Behavior3/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines3/5

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.

  1. 3 tool updatesv0.1.0
    • First observedexplain
    • First observedlist_tables
    • First observedping

TDQS

A4.2/5.0

Scored across 3 tools

Disambiguation5/5

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.

Naming Consistency4/5

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.

Tool Count5/5

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.

Completeness5/5

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

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    A
    quality
    D
    maintenance
    Enables AI assistants to interact with PostgreSQL databases using natural language queries, providing secure read-only access to database schemas and SQL translation capabilities.
    6
    11 npm
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables 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 npm
    Apache 2.0
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables AI assistants to safely interact with PostgreSQL databases, perform queries, inspect schemas, and analyze query performance.
    2
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Securely connect AI assistants to PostgreSQL databases with read-only access, schema discovery, querying, and performance analysis tools.
    6 npm
    MIT