dataxplan-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataxplan_mcp-0.1.0/LICENSE +21 -0
- dataxplan_mcp-0.1.0/PKG-INFO +141 -0
- dataxplan_mcp-0.1.0/README.md +109 -0
- dataxplan_mcp-0.1.0/dataxplan_mcp/__init__.py +5 -0
- dataxplan_mcp-0.1.0/dataxplan_mcp/_tools.py +137 -0
- dataxplan_mcp-0.1.0/dataxplan_mcp/_version.py +1 -0
- dataxplan_mcp-0.1.0/dataxplan_mcp/server.py +45 -0
- dataxplan_mcp-0.1.0/dataxplan_mcp.egg-info/PKG-INFO +141 -0
- dataxplan_mcp-0.1.0/dataxplan_mcp.egg-info/SOURCES.txt +15 -0
- dataxplan_mcp-0.1.0/dataxplan_mcp.egg-info/dependency_links.txt +1 -0
- dataxplan_mcp-0.1.0/dataxplan_mcp.egg-info/entry_points.txt +2 -0
- dataxplan_mcp-0.1.0/dataxplan_mcp.egg-info/requires.txt +8 -0
- dataxplan_mcp-0.1.0/dataxplan_mcp.egg-info/top_level.txt +1 -0
- dataxplan_mcp-0.1.0/pyproject.toml +52 -0
- dataxplan_mcp-0.1.0/setup.cfg +4 -0
- dataxplan_mcp-0.1.0/tests/test_server.py +18 -0
- dataxplan_mcp-0.1.0/tests/test_tools.py +82 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Atakan Arikan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dataxplan-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for dataxplan: read PostgreSQL EXPLAIN plans for AI agents (bottlenecks, estimation errors, fixes, charts).
|
|
5
|
+
Author: Atakan Arikan
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/arikanatakan/dataxplan-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/arikanatakan/dataxplan-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/arikanatakan/dataxplan-mcp/issues
|
|
10
|
+
Project-URL: Library, https://github.com/arikanatakan/dataxplan
|
|
11
|
+
Keywords: mcp,model-context-protocol,ai-agents,postgresql,postgres,explain,query-plan,performance,query-optimization,database
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Database
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: dataxplan[viz,yaml]>=0.1.3
|
|
25
|
+
Requires-Dist: mcp>=1.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy; extra == "dev"
|
|
30
|
+
Requires-Dist: build; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
<!-- mcp-name: io.github.arikanatakan/dataxplan-mcp -->
|
|
34
|
+
|
|
35
|
+
# dataxplan-mcp
|
|
36
|
+
|
|
37
|
+
[](https://github.com/arikanatakan/dataxplan-mcp/actions/workflows/ci.yml)
|
|
38
|
+
[](https://pypi.org/project/dataxplan-mcp/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
An MCP server that exposes [dataxplan](https://github.com/arikanatakan/dataxplan),
|
|
42
|
+
the PostgreSQL EXPLAIN-plan analyzer for Python, as tools for AI agents: give it
|
|
43
|
+
the output of `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` and it returns the
|
|
44
|
+
bottlenecks, the estimation errors, documented findings with a suggested action
|
|
45
|
+
and a source reference, a regression comparison, and a self-time chart.
|
|
46
|
+
|
|
47
|
+
Agents asked why a query is slow tend to eyeball the plan and get it wrong: self
|
|
48
|
+
time is per loop and inclusive of children, so the slow node is rarely the
|
|
49
|
+
obvious one, and a row mis-estimate (the usual root cause) is buried in the
|
|
50
|
+
output. Reading the plan belongs in a deterministic, versioned library that the
|
|
51
|
+
agent calls, which leaves the agent to interpret the result and decide. The
|
|
52
|
+
server never connects to a database: the agent runs EXPLAIN and passes the
|
|
53
|
+
output, so nothing leaves its environment.
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
## Tools
|
|
58
|
+
|
|
59
|
+
**Analysis tools** return dataxplan's payload: the metrics, the findings (each
|
|
60
|
+
with a severity, a suggestion and a documented source reference) and a summary.
|
|
61
|
+
|
|
62
|
+
| Tool | Purpose |
|
|
63
|
+
| ---- | ------- |
|
|
64
|
+
| `analyze_plan` | bottlenecks, estimation errors and findings from an EXPLAIN plan (JSON, text, YAML or XML) |
|
|
65
|
+
| `compare_plans` | compare two plans for regression (timing, shape, estimates, findings) |
|
|
66
|
+
| `plan_tree` | the plan as an annotated text tree (self time, rows, flags per node) |
|
|
67
|
+
| `describe_inputs` | how to produce the plan, the accepted formats, the findings and the thresholds |
|
|
68
|
+
|
|
69
|
+
**Chart tools** return a PNG image.
|
|
70
|
+
|
|
71
|
+
| Tool | Purpose |
|
|
72
|
+
| ---- | ------- |
|
|
73
|
+
| `plan_chart` | self time per node, with the high-severity findings highlighted |
|
|
74
|
+
|
|
75
|
+
All tools are read-only, and the server makes no database connection.
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
Run it with [uv](https://docs.astral.sh/uv/) (no install needed):
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
uvx dataxplan-mcp
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
or install from PyPI:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
pip install dataxplan-mcp
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Configuration
|
|
92
|
+
|
|
93
|
+
Add it to your MCP client. For example:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"dataxplan": {
|
|
99
|
+
"command": "uvx",
|
|
100
|
+
"args": ["dataxplan-mcp"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
If you installed with pip, use `"command": "dataxplan-mcp"` with no args.
|
|
107
|
+
|
|
108
|
+
## Example
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
analyze_plan(plan="<EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) output>")
|
|
112
|
+
-> { "summary_metrics": { "execution_time_ms": 1240.0,
|
|
113
|
+
"max_estimation_error": 20000, ... },
|
|
114
|
+
"findings": [ { "id": "seq_scan_hot", "severity": "high",
|
|
115
|
+
"detail": "... 95% of execution time ...",
|
|
116
|
+
"suggestion": "consider an index ...",
|
|
117
|
+
"reference": "PostgreSQL: Using EXPLAIN; the Indexes chapter" } ],
|
|
118
|
+
"summary": "dataxplan - ...\n execution time ..." }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The agent runs the EXPLAIN itself and pastes the output (any format) as `plan`.
|
|
122
|
+
|
|
123
|
+
## Design
|
|
124
|
+
|
|
125
|
+
The server is a thin, stateless wrapper. All of the analysis lives in the
|
|
126
|
+
dataxplan library, which computes the metrics from the documented EXPLAIN fields
|
|
127
|
+
and grounds each finding in the PostgreSQL manual (and Leis et al. 2015 for the
|
|
128
|
+
estimation rules). The server adds the tool schema, read-only annotations and an
|
|
129
|
+
input-schema helper so an agent can format the input and act on the result. The
|
|
130
|
+
findings are documented heuristics, not guarantees, and the server connects to
|
|
131
|
+
nothing.
|
|
132
|
+
|
|
133
|
+
## Related
|
|
134
|
+
|
|
135
|
+
- [dataxplan](https://github.com/arikanatakan/dataxplan): the library this server
|
|
136
|
+
wraps.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT. Written and maintained by [Atakan Arikan](https://github.com/arikanatakan),
|
|
141
|
+
MSc Student at Tsinghua University and Politecnico di Milano.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.arikanatakan/dataxplan-mcp -->
|
|
2
|
+
|
|
3
|
+
# dataxplan-mcp
|
|
4
|
+
|
|
5
|
+
[](https://github.com/arikanatakan/dataxplan-mcp/actions/workflows/ci.yml)
|
|
6
|
+
[](https://pypi.org/project/dataxplan-mcp/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
An MCP server that exposes [dataxplan](https://github.com/arikanatakan/dataxplan),
|
|
10
|
+
the PostgreSQL EXPLAIN-plan analyzer for Python, as tools for AI agents: give it
|
|
11
|
+
the output of `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` and it returns the
|
|
12
|
+
bottlenecks, the estimation errors, documented findings with a suggested action
|
|
13
|
+
and a source reference, a regression comparison, and a self-time chart.
|
|
14
|
+
|
|
15
|
+
Agents asked why a query is slow tend to eyeball the plan and get it wrong: self
|
|
16
|
+
time is per loop and inclusive of children, so the slow node is rarely the
|
|
17
|
+
obvious one, and a row mis-estimate (the usual root cause) is buried in the
|
|
18
|
+
output. Reading the plan belongs in a deterministic, versioned library that the
|
|
19
|
+
agent calls, which leaves the agent to interpret the result and decide. The
|
|
20
|
+
server never connects to a database: the agent runs EXPLAIN and passes the
|
|
21
|
+
output, so nothing leaves its environment.
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
## Tools
|
|
26
|
+
|
|
27
|
+
**Analysis tools** return dataxplan's payload: the metrics, the findings (each
|
|
28
|
+
with a severity, a suggestion and a documented source reference) and a summary.
|
|
29
|
+
|
|
30
|
+
| Tool | Purpose |
|
|
31
|
+
| ---- | ------- |
|
|
32
|
+
| `analyze_plan` | bottlenecks, estimation errors and findings from an EXPLAIN plan (JSON, text, YAML or XML) |
|
|
33
|
+
| `compare_plans` | compare two plans for regression (timing, shape, estimates, findings) |
|
|
34
|
+
| `plan_tree` | the plan as an annotated text tree (self time, rows, flags per node) |
|
|
35
|
+
| `describe_inputs` | how to produce the plan, the accepted formats, the findings and the thresholds |
|
|
36
|
+
|
|
37
|
+
**Chart tools** return a PNG image.
|
|
38
|
+
|
|
39
|
+
| Tool | Purpose |
|
|
40
|
+
| ---- | ------- |
|
|
41
|
+
| `plan_chart` | self time per node, with the high-severity findings highlighted |
|
|
42
|
+
|
|
43
|
+
All tools are read-only, and the server makes no database connection.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
Run it with [uv](https://docs.astral.sh/uv/) (no install needed):
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
uvx dataxplan-mcp
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
or install from PyPI:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
pip install dataxplan-mcp
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
Add it to your MCP client. For example:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"dataxplan": {
|
|
67
|
+
"command": "uvx",
|
|
68
|
+
"args": ["dataxplan-mcp"]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If you installed with pip, use `"command": "dataxplan-mcp"` with no args.
|
|
75
|
+
|
|
76
|
+
## Example
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
analyze_plan(plan="<EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) output>")
|
|
80
|
+
-> { "summary_metrics": { "execution_time_ms": 1240.0,
|
|
81
|
+
"max_estimation_error": 20000, ... },
|
|
82
|
+
"findings": [ { "id": "seq_scan_hot", "severity": "high",
|
|
83
|
+
"detail": "... 95% of execution time ...",
|
|
84
|
+
"suggestion": "consider an index ...",
|
|
85
|
+
"reference": "PostgreSQL: Using EXPLAIN; the Indexes chapter" } ],
|
|
86
|
+
"summary": "dataxplan - ...\n execution time ..." }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The agent runs the EXPLAIN itself and pastes the output (any format) as `plan`.
|
|
90
|
+
|
|
91
|
+
## Design
|
|
92
|
+
|
|
93
|
+
The server is a thin, stateless wrapper. All of the analysis lives in the
|
|
94
|
+
dataxplan library, which computes the metrics from the documented EXPLAIN fields
|
|
95
|
+
and grounds each finding in the PostgreSQL manual (and Leis et al. 2015 for the
|
|
96
|
+
estimation rules). The server adds the tool schema, read-only annotations and an
|
|
97
|
+
input-schema helper so an agent can format the input and act on the result. The
|
|
98
|
+
findings are documented heuristics, not guarantees, and the server connects to
|
|
99
|
+
nothing.
|
|
100
|
+
|
|
101
|
+
## Related
|
|
102
|
+
|
|
103
|
+
- [dataxplan](https://github.com/arikanatakan/dataxplan): the library this server
|
|
104
|
+
wraps.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT. Written and maintained by [Atakan Arikan](https://github.com/arikanatakan),
|
|
109
|
+
MSc Student at Tsinghua University and Politecnico di Milano.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Tool logic, kept free of the MCP SDK so it can be tested directly.
|
|
2
|
+
|
|
3
|
+
Each tool calls the dataxplan library on the EXPLAIN plan the agent supplies and
|
|
4
|
+
returns its JSON-safe payload (metrics, findings with a suggestion and a source
|
|
5
|
+
reference) plus a plain-language summary. The server never connects to a
|
|
6
|
+
database: the agent runs EXPLAIN and passes the output here.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import io
|
|
12
|
+
|
|
13
|
+
import matplotlib
|
|
14
|
+
|
|
15
|
+
matplotlib.use("Agg")
|
|
16
|
+
|
|
17
|
+
import dataxplan # noqa: E402
|
|
18
|
+
from dataxplan.findings import DEFAULT_THRESHOLDS # noqa: E402
|
|
19
|
+
from pydantic import BaseModel, Field # noqa: E402
|
|
20
|
+
|
|
21
|
+
_HINT = "call describe_inputs for how to produce the plan and the accepted formats"
|
|
22
|
+
|
|
23
|
+
FINDINGS = {
|
|
24
|
+
"estimate_off": "the actual rows are far from the estimate (the usual root "
|
|
25
|
+
"cause of a bad plan)",
|
|
26
|
+
"seq_scan_hot": "a sequential scan is a large share of a non-trivial query's time",
|
|
27
|
+
"disk_spill": "a sort or hash spilled to disk (work_mem too small)",
|
|
28
|
+
"filter_discard": "a scan read many rows and kept few (non-sargable or a "
|
|
29
|
+
"missing index)",
|
|
30
|
+
"nested_loop_blowup": "a nested loop ran its inner side many times and it cost "
|
|
31
|
+
"real time",
|
|
32
|
+
"index_only_heap_fetches": "an index-only scan still hit the heap (the table "
|
|
33
|
+
"needs VACUUM)",
|
|
34
|
+
"lossy_bitmap": "a bitmap heap scan went lossy (work_mem too small)",
|
|
35
|
+
"jit_overhead": "JIT compilation was a large share of a short query's time",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
NOTES = (
|
|
39
|
+
"The findings are documented heuristics, not guarantees, and the analysis is "
|
|
40
|
+
"of the plan you provide: the server does not connect to a database, run your "
|
|
41
|
+
"query or read your schema. Self times for parallel plans are total work "
|
|
42
|
+
"across workers, not wall-clock time. Run EXPLAIN with ANALYZE for timing."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TableInfoInput(BaseModel):
|
|
47
|
+
"""Optional catalog facts about one table."""
|
|
48
|
+
|
|
49
|
+
row_count: float | None = Field(default=None, description="Approximate row count.")
|
|
50
|
+
indexed_columns: list[str] = Field(
|
|
51
|
+
default_factory=list, description="Columns that appear in some index.")
|
|
52
|
+
analyzed: bool = Field(default=True, description="False if statistics look stale.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ContextInput(BaseModel):
|
|
56
|
+
"""Optional catalog context that sharpens the findings."""
|
|
57
|
+
|
|
58
|
+
tables: dict[str, TableInfoInput] = Field(
|
|
59
|
+
default_factory=dict, description="Table name -> catalog facts.")
|
|
60
|
+
work_mem_mb: float | None = Field(default=None, description="The server's work_mem in MB.")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _context(context: ContextInput | None):
|
|
64
|
+
if context is None:
|
|
65
|
+
return None
|
|
66
|
+
return {
|
|
67
|
+
"tables": {
|
|
68
|
+
name: {"row_count": t.row_count,
|
|
69
|
+
"indexed_columns": tuple(t.indexed_columns),
|
|
70
|
+
"analyzed": t.analyzed}
|
|
71
|
+
for name, t in context.tables.items()
|
|
72
|
+
},
|
|
73
|
+
"work_mem_mb": context.work_mem_mb,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _payload(result) -> dict:
|
|
78
|
+
out = result.to_dict()
|
|
79
|
+
out["summary"] = result.summary()
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def analyze_plan(plan: str, context: ContextInput | None = None,
|
|
84
|
+
thresholds: dict[str, float] | None = None) -> dict:
|
|
85
|
+
"""Analyse a PostgreSQL EXPLAIN plan: bottlenecks, estimation errors and findings.
|
|
86
|
+
|
|
87
|
+
``plan`` is the output of ``EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ...`` (text,
|
|
88
|
+
YAML and XML are also accepted). Optional ``context`` (table sizes, indexed
|
|
89
|
+
columns, stale stats) sharpens the findings; ``thresholds`` overrides the rule
|
|
90
|
+
cut-offs (see describe_inputs).
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
report = dataxplan.analyze(plan, _context(context), thresholds=thresholds)
|
|
94
|
+
return _payload(report)
|
|
95
|
+
except (ValueError, TypeError, ImportError) as exc:
|
|
96
|
+
return {"error": str(exc), "hint": _HINT}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def compare_plans(before: str, after: str) -> dict:
|
|
100
|
+
"""Compare two plans for regression: timing, plan shape, estimates and findings."""
|
|
101
|
+
try:
|
|
102
|
+
return _payload(dataxplan.compare(before, after))
|
|
103
|
+
except (ValueError, TypeError) as exc:
|
|
104
|
+
return {"error": str(exc), "hint": _HINT}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def plan_tree(plan: str) -> str:
|
|
108
|
+
"""The plan as an annotated text tree (self time, rows, flags per node)."""
|
|
109
|
+
try:
|
|
110
|
+
return dataxplan.text_tree(dataxplan.analyze(plan))
|
|
111
|
+
except (ValueError, TypeError) as exc:
|
|
112
|
+
return f"error: {exc}\n({_HINT})"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def describe_inputs() -> dict:
|
|
116
|
+
"""How to produce the plan, the accepted formats, the findings and thresholds."""
|
|
117
|
+
return {
|
|
118
|
+
"how_to": "Run EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) <query> and pass "
|
|
119
|
+
"its output as `plan`. ANALYZE adds the real times and row "
|
|
120
|
+
"counts (needed for self time and most findings); BUFFERS adds "
|
|
121
|
+
"the block counts.",
|
|
122
|
+
"accepted_formats": ["JSON (exact)", "text (best-effort)", "YAML", "XML"],
|
|
123
|
+
"findings": FINDINGS,
|
|
124
|
+
"thresholds": DEFAULT_THRESHOLDS,
|
|
125
|
+
"notes": NOTES,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def plan_png(plan: str) -> bytes:
|
|
130
|
+
"""Render the self-time-per-node chart as PNG bytes."""
|
|
131
|
+
report = dataxplan.analyze(plan)
|
|
132
|
+
fig = dataxplan.plan_tree_chart(report)
|
|
133
|
+
buffer = io.BytesIO()
|
|
134
|
+
fig.savefig(buffer, format="png", dpi=150, bbox_inches="tight", facecolor="white")
|
|
135
|
+
import matplotlib.pyplot as plt
|
|
136
|
+
plt.close(fig)
|
|
137
|
+
return buffer.getvalue()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""The MCP server: registers the dataxplan tools and runs over stdio.
|
|
2
|
+
|
|
3
|
+
All tools are pure, read-only computations on the plan the agent supplies; the
|
|
4
|
+
server never connects to a database. They are marked with annotations so a
|
|
5
|
+
client can present and auto-run them safely.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from mcp.server.fastmcp import FastMCP, Image
|
|
11
|
+
from mcp.types import ToolAnnotations
|
|
12
|
+
|
|
13
|
+
from . import _tools
|
|
14
|
+
|
|
15
|
+
mcp = FastMCP("dataxplan")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _annotations(title: str) -> ToolAnnotations:
|
|
19
|
+
return ToolAnnotations(
|
|
20
|
+
title=title,
|
|
21
|
+
readOnlyHint=True,
|
|
22
|
+
idempotentHint=True,
|
|
23
|
+
openWorldHint=False,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
mcp.tool(annotations=_annotations("Analyse an EXPLAIN plan"))(_tools.analyze_plan)
|
|
28
|
+
mcp.tool(annotations=_annotations("Compare two plans (regression)"))(_tools.compare_plans)
|
|
29
|
+
mcp.tool(annotations=_annotations("Annotated plan tree"))(_tools.plan_tree)
|
|
30
|
+
mcp.tool(annotations=_annotations("Describe the inputs"))(_tools.describe_inputs)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@mcp.tool(annotations=_annotations("Self-time chart (PNG)"))
|
|
34
|
+
def plan_chart(plan: str) -> Image:
|
|
35
|
+
"""Render the self-time-per-node chart for a plan as a PNG image."""
|
|
36
|
+
return Image(data=_tools.plan_png(plan), format="png")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main() -> None:
|
|
40
|
+
"""Console-script entry point: run the server on stdio."""
|
|
41
|
+
mcp.run()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
main()
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dataxplan-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for dataxplan: read PostgreSQL EXPLAIN plans for AI agents (bottlenecks, estimation errors, fixes, charts).
|
|
5
|
+
Author: Atakan Arikan
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/arikanatakan/dataxplan-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/arikanatakan/dataxplan-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/arikanatakan/dataxplan-mcp/issues
|
|
10
|
+
Project-URL: Library, https://github.com/arikanatakan/dataxplan
|
|
11
|
+
Keywords: mcp,model-context-protocol,ai-agents,postgresql,postgres,explain,query-plan,performance,query-optimization,database
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Database
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: dataxplan[viz,yaml]>=0.1.3
|
|
25
|
+
Requires-Dist: mcp>=1.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy; extra == "dev"
|
|
30
|
+
Requires-Dist: build; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
<!-- mcp-name: io.github.arikanatakan/dataxplan-mcp -->
|
|
34
|
+
|
|
35
|
+
# dataxplan-mcp
|
|
36
|
+
|
|
37
|
+
[](https://github.com/arikanatakan/dataxplan-mcp/actions/workflows/ci.yml)
|
|
38
|
+
[](https://pypi.org/project/dataxplan-mcp/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
An MCP server that exposes [dataxplan](https://github.com/arikanatakan/dataxplan),
|
|
42
|
+
the PostgreSQL EXPLAIN-plan analyzer for Python, as tools for AI agents: give it
|
|
43
|
+
the output of `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` and it returns the
|
|
44
|
+
bottlenecks, the estimation errors, documented findings with a suggested action
|
|
45
|
+
and a source reference, a regression comparison, and a self-time chart.
|
|
46
|
+
|
|
47
|
+
Agents asked why a query is slow tend to eyeball the plan and get it wrong: self
|
|
48
|
+
time is per loop and inclusive of children, so the slow node is rarely the
|
|
49
|
+
obvious one, and a row mis-estimate (the usual root cause) is buried in the
|
|
50
|
+
output. Reading the plan belongs in a deterministic, versioned library that the
|
|
51
|
+
agent calls, which leaves the agent to interpret the result and decide. The
|
|
52
|
+
server never connects to a database: the agent runs EXPLAIN and passes the
|
|
53
|
+
output, so nothing leaves its environment.
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
## Tools
|
|
58
|
+
|
|
59
|
+
**Analysis tools** return dataxplan's payload: the metrics, the findings (each
|
|
60
|
+
with a severity, a suggestion and a documented source reference) and a summary.
|
|
61
|
+
|
|
62
|
+
| Tool | Purpose |
|
|
63
|
+
| ---- | ------- |
|
|
64
|
+
| `analyze_plan` | bottlenecks, estimation errors and findings from an EXPLAIN plan (JSON, text, YAML or XML) |
|
|
65
|
+
| `compare_plans` | compare two plans for regression (timing, shape, estimates, findings) |
|
|
66
|
+
| `plan_tree` | the plan as an annotated text tree (self time, rows, flags per node) |
|
|
67
|
+
| `describe_inputs` | how to produce the plan, the accepted formats, the findings and the thresholds |
|
|
68
|
+
|
|
69
|
+
**Chart tools** return a PNG image.
|
|
70
|
+
|
|
71
|
+
| Tool | Purpose |
|
|
72
|
+
| ---- | ------- |
|
|
73
|
+
| `plan_chart` | self time per node, with the high-severity findings highlighted |
|
|
74
|
+
|
|
75
|
+
All tools are read-only, and the server makes no database connection.
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
Run it with [uv](https://docs.astral.sh/uv/) (no install needed):
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
uvx dataxplan-mcp
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
or install from PyPI:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
pip install dataxplan-mcp
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Configuration
|
|
92
|
+
|
|
93
|
+
Add it to your MCP client. For example:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"dataxplan": {
|
|
99
|
+
"command": "uvx",
|
|
100
|
+
"args": ["dataxplan-mcp"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
If you installed with pip, use `"command": "dataxplan-mcp"` with no args.
|
|
107
|
+
|
|
108
|
+
## Example
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
analyze_plan(plan="<EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) output>")
|
|
112
|
+
-> { "summary_metrics": { "execution_time_ms": 1240.0,
|
|
113
|
+
"max_estimation_error": 20000, ... },
|
|
114
|
+
"findings": [ { "id": "seq_scan_hot", "severity": "high",
|
|
115
|
+
"detail": "... 95% of execution time ...",
|
|
116
|
+
"suggestion": "consider an index ...",
|
|
117
|
+
"reference": "PostgreSQL: Using EXPLAIN; the Indexes chapter" } ],
|
|
118
|
+
"summary": "dataxplan - ...\n execution time ..." }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The agent runs the EXPLAIN itself and pastes the output (any format) as `plan`.
|
|
122
|
+
|
|
123
|
+
## Design
|
|
124
|
+
|
|
125
|
+
The server is a thin, stateless wrapper. All of the analysis lives in the
|
|
126
|
+
dataxplan library, which computes the metrics from the documented EXPLAIN fields
|
|
127
|
+
and grounds each finding in the PostgreSQL manual (and Leis et al. 2015 for the
|
|
128
|
+
estimation rules). The server adds the tool schema, read-only annotations and an
|
|
129
|
+
input-schema helper so an agent can format the input and act on the result. The
|
|
130
|
+
findings are documented heuristics, not guarantees, and the server connects to
|
|
131
|
+
nothing.
|
|
132
|
+
|
|
133
|
+
## Related
|
|
134
|
+
|
|
135
|
+
- [dataxplan](https://github.com/arikanatakan/dataxplan): the library this server
|
|
136
|
+
wraps.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT. Written and maintained by [Atakan Arikan](https://github.com/arikanatakan),
|
|
141
|
+
MSc Student at Tsinghua University and Politecnico di Milano.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
dataxplan_mcp/__init__.py
|
|
5
|
+
dataxplan_mcp/_tools.py
|
|
6
|
+
dataxplan_mcp/_version.py
|
|
7
|
+
dataxplan_mcp/server.py
|
|
8
|
+
dataxplan_mcp.egg-info/PKG-INFO
|
|
9
|
+
dataxplan_mcp.egg-info/SOURCES.txt
|
|
10
|
+
dataxplan_mcp.egg-info/dependency_links.txt
|
|
11
|
+
dataxplan_mcp.egg-info/entry_points.txt
|
|
12
|
+
dataxplan_mcp.egg-info/requires.txt
|
|
13
|
+
dataxplan_mcp.egg-info/top_level.txt
|
|
14
|
+
tests/test_server.py
|
|
15
|
+
tests/test_tools.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dataxplan_mcp
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dataxplan-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for dataxplan: read PostgreSQL EXPLAIN plans for AI agents (bottlenecks, estimation errors, fixes, charts)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Atakan Arikan" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = ["dataxplan[viz,yaml]>=0.1.3", "mcp>=1.0"]
|
|
14
|
+
keywords = [
|
|
15
|
+
"mcp", "model-context-protocol", "ai-agents", "postgresql", "postgres",
|
|
16
|
+
"explain", "query-plan", "performance", "query-optimization", "database",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Intended Audience :: Information Technology",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: Database",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
dataxplan-mcp = "dataxplan_mcp.server:main"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = ["pytest>=7", "ruff", "mypy", "build"]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/arikanatakan/dataxplan-mcp"
|
|
38
|
+
Repository = "https://github.com/arikanatakan/dataxplan-mcp"
|
|
39
|
+
Issues = "https://github.com/arikanatakan/dataxplan-mcp/issues"
|
|
40
|
+
Library = "https://github.com/arikanatakan/dataxplan"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
include = ["dataxplan_mcp*"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
line-length = 88
|
|
47
|
+
|
|
48
|
+
[tool.mypy]
|
|
49
|
+
ignore_missing_imports = true
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
def test_server_imports_and_wires():
|
|
2
|
+
from dataxplan_mcp import server
|
|
3
|
+
|
|
4
|
+
assert server.mcp is not None
|
|
5
|
+
assert callable(server.main)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_all_tools_registered():
|
|
9
|
+
import asyncio
|
|
10
|
+
|
|
11
|
+
from dataxplan_mcp import server
|
|
12
|
+
|
|
13
|
+
names = {tool.name for tool in asyncio.run(server.mcp.list_tools())}
|
|
14
|
+
expected = {
|
|
15
|
+
"analyze_plan", "compare_plans", "plan_tree", "describe_inputs",
|
|
16
|
+
"plan_chart",
|
|
17
|
+
}
|
|
18
|
+
assert expected <= names
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from dataxplan_mcp import _tools as t
|
|
4
|
+
from dataxplan_mcp._tools import ContextInput, TableInfoInput
|
|
5
|
+
|
|
6
|
+
PLAN = json.dumps([{
|
|
7
|
+
"Plan": {
|
|
8
|
+
"Node Type": "Seq Scan", "Relation Name": "orders",
|
|
9
|
+
"Startup Cost": 0.0, "Total Cost": 35811.0, "Plan Rows": 5,
|
|
10
|
+
"Plan Width": 244, "Actual Startup Time": 0.03,
|
|
11
|
+
"Actual Total Time": 900.0, "Actual Rows": 5, "Actual Loops": 1,
|
|
12
|
+
"Filter": "(status = 'X')", "Rows Removed by Filter": 10000000},
|
|
13
|
+
"Planning Time": 0.4, "Execution Time": 905.0}])
|
|
14
|
+
|
|
15
|
+
FAST = json.dumps([{
|
|
16
|
+
"Plan": {
|
|
17
|
+
"Node Type": "Index Scan", "Relation Name": "orders",
|
|
18
|
+
"Index Name": "orders_pkey", "Plan Rows": 1, "Plan Width": 244,
|
|
19
|
+
"Total Cost": 8.3, "Actual Startup Time": 0.02, "Actual Total Time": 0.05,
|
|
20
|
+
"Actual Rows": 1, "Actual Loops": 1},
|
|
21
|
+
"Execution Time": 0.08}])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _ids(payload):
|
|
25
|
+
return {f["id"] for f in payload["findings"]}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_analyze_plan():
|
|
29
|
+
r = t.analyze_plan(PLAN)
|
|
30
|
+
assert "summary" in r
|
|
31
|
+
ids = _ids(r)
|
|
32
|
+
assert "seq_scan_hot" in ids and "filter_discard" in ids
|
|
33
|
+
seq = next(f for f in r["findings"] if f["id"] == "seq_scan_hot")
|
|
34
|
+
assert seq["reference"] and seq["suggestion"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_analyze_plan_error_has_hint():
|
|
38
|
+
r = t.analyze_plan("not a plan at all")
|
|
39
|
+
assert "error" in r and "describe_inputs" in r["hint"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_analyze_with_context_sharpens():
|
|
43
|
+
ctx = ContextInput(tables={"orders": TableInfoInput(
|
|
44
|
+
row_count=10_000_000, indexed_columns=["id"])})
|
|
45
|
+
r = t.analyze_plan(PLAN, context=ctx)
|
|
46
|
+
seq = next(f for f in r["findings"] if f["id"] == "seq_scan_hot")
|
|
47
|
+
assert "10,000,000 rows" in seq["detail"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_analyze_accepts_text_format():
|
|
51
|
+
text = ("Seq Scan on orders (cost=0.00..35811.00 rows=5 width=244) "
|
|
52
|
+
"(actual time=0.030..900.000 rows=5 loops=1)\n"
|
|
53
|
+
" Rows Removed by Filter: 10000000\n"
|
|
54
|
+
"Execution Time: 905.000 ms\n")
|
|
55
|
+
assert "seq_scan_hot" in _ids(t.analyze_plan(text))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_compare_plans():
|
|
59
|
+
r = t.compare_plans(PLAN, FAST)
|
|
60
|
+
assert r["verdict"] == "improved"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_plan_tree():
|
|
64
|
+
tree = t.plan_tree(PLAN)
|
|
65
|
+
assert "Seq Scan on orders" in tree
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_describe_inputs():
|
|
69
|
+
d = t.describe_inputs()
|
|
70
|
+
assert d["findings"] and d["thresholds"] and d["accepted_formats"]
|
|
71
|
+
assert "min_time_ms" in d["thresholds"]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_payload_is_json_serializable():
|
|
75
|
+
json.dumps(t.analyze_plan(PLAN))
|
|
76
|
+
json.dumps(t.compare_plans(PLAN, FAST))
|
|
77
|
+
json.dumps(t.describe_inputs())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_plan_png():
|
|
81
|
+
png = t.plan_png(PLAN)
|
|
82
|
+
assert isinstance(png, bytes) and png[:4] == b"\x89PNG"
|