query-executor
README.md
# Query Executor MCP Server
A [Model Context Protocol](https://modelcontextprotocol.io) server that gives AI agents read (and optionally write) access to one or more PostgreSQL databases. Each database is registered as a named **project** — the agent picks the right one per call via `project_id`.
---
## How it works
```mermaid
flowchart TD
A[AI Agent\nClaude / Cursor / etc.] -->|MCP stdio| B[Query Executor\nMCP Server]
B --> C{Resolve project_id}
C -->|default| D[(default DB)]
C -->|project-alpha| E[(project-alpha DB)]
C -->|project-beta| F[(project-beta DB)]
subgraph Tools
T1[describe_postgres_schema]
T2[execute_postgres]
T3[explain_postgres]
T4[pg_stat_statements]
end
B --> Tools
Tools --> C
```
```mermaid
sequenceDiagram
participant Agent
participant Server as MCP Server
participant Config as databases.json
participant DB as PostgreSQL
Agent->>Server: tool call (project_id, sql)
Server->>Config: resolve project_id → DSN + mode
Config-->>Server: { dsn, mode }
alt mode = readonly
Server->>Server: assert first token is SELECT/WITH
end
Server->>DB: execute query
DB-->>Server: rows
Server-->>Agent: JSON { rows, row_count }
```
---
## Features
- **Multi-project** — connect to any number of PostgreSQL databases simultaneously; each call targets one via `project_id`
- **Per-project mode** — `readonly` blocks all writes at the first SQL token; `readwrite` allows all SQL
- **Safe fallback** — omitting `project_id` routes to the configured default project
- **Four tools** — schema inspection, query execution, EXPLAIN ANALYZE, and slow-query analysis
- **stdio transport** — works with any MCP client (Claude Desktop, Cursor, Claude Code, etc.)
- **Docker-ready** — single image, credentials baked in at build time from gitignored files
---
## Project layout
```
query-executor/
├── Dockerfile
├── Makefile
├── pyproject.toml
├── uv.lock
├── main.py # connection check entrypoint
├── .env # gitignored — copy from .env.example
├── databases.json # gitignored — copy from databases.example.json
├── .env.example
├── databases.example.json
└── query_executor/
├── config.py # loads .env + databases.json; single source of truth
├── query_connector.py # raw asyncpg functions (no MCP imports)
├── tools.py # Pydantic input models + tool implementations
└── server.py # FastMCP bootstrap + entrypoint
```
---
## Quick start
**Requires:** [UV](https://docs.astral.sh/uv/) and [Docker](https://docs.docker.com/get-docker/).
### 1. Install dependencies
```bash
uv sync
```
### 2. Configure
```bash
cp .env.example .env
cp databases.example.json databases.json
```
Edit `databases.json` with your real connection strings:
```json
{
"default": {
"dsn": "postgresql://user:password@localhost:5432/mydb",
"mode": "readonly"
},
"staging": {
"dsn": "postgresql://user:password@staging-host:5432/stagingdb",
"mode": "readwrite"
}
}
```
| `mode` | Behaviour |
|-------------|-----------|
| `readonly` | Only `SELECT` / `WITH` queries are allowed. Any write or DDL raises an error before reaching the database. |
| `readwrite` | All SQL is permitted. Use only on non-production databases. |
If a project does not specify `mode`, it defaults to `readonly`.
### 3. Check connections
```bash
make check
```
```
Welcome to Query Executor!
Default project : default
• default [readonly] (default)
• staging [readwrite]
Testing database connections...
[default] mode=readonly ... OK — PostgreSQL 15.4
[staging] mode=readwrite ... OK — PostgreSQL 15.4
All 2 connection(s) OK.
```
### 4. Build the Docker image
```bash
make build
```
---
## MCP client configuration
The server runs over **stdio** — the MCP client spawns the process and communicates via stdin/stdout.
### Claude Desktop
Config file: `~/Library/Application Support/Claude/claude_desktop_config.json`
**With UV (local):**
```json
{
"mcpServers": {
"query-executor": {
"command": "uv",
"args": ["run", "python", "-m", "query_executor.server"],
"cwd": "/Users/niteshnandan/workspace/2026/query-executor"
}
}
}
```
**With Docker:**
```json
{
"mcpServers": {
"query-executor": {
"command": "docker",
"args": ["run", "-i", "--rm", "query-executor"]
}
}
}
```
### Cursor
Global config: `~/.cursor/mcp.json`
Project config: `.cursor/mcp.json`
**With UV (local):**
```json
{
"mcpServers": {
"query-executor": {
"command": "uv",
"args": ["run", "python", "-m", "query_executor.server"],
"cwd": "/Users/niteshnandan/workspace/2026/query-executor"
}
}
}
```
**With Docker:**
```json
{
"mcpServers": {
"query-executor": {
"command": "docker",
"args": ["run", "-i", "--rm", "query-executor"]
}
}
}
```
### Claude Code
Add to your project's `.claude/mcp.json`:
**With UV (local):**
```json
{
"mcpServers": {
"query-executor": {
"type": "stdio",
"command": "uv",
"args": ["run", "python", "-m", "query_executor.server"],
"cwd": "/Users/niteshnandan/workspace/2026/query-executor"
}
}
}
```
**With Docker:**
```json
{
"mcpServers": {
"query-executor": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "query-executor"]
}
}
}
```
> **Note:** Replace `/Users/niteshnandan/workspace/2026/query-executor` with the actual path on your machine if sharing config with others.
---
## Tools reference
### `describe_postgres_schema`
Inspect tables, columns, foreign keys, and indexes for a given schema.
**Call this first** before writing any query — it gives you exact column names and types so you write correct SQL on the first attempt.
| Parameter | Type | Default | Description |
|------------------|--------|-----------------|-------------|
| `postgres_schema`| string | `"public"` | Schema to inspect |
| `project_id` | string | default project | Target database |
Returns `{ columns, foreign_keys, indexes }`.
---
### `execute_postgres`
Run a SQL query and get rows back as JSON.
| Parameter | Type | Required | Description |
|--------------|--------|----------|-------------|
| `sql` | string | yes | SQL to execute. In `readonly` mode only SELECT/WITH are allowed. |
| `project_id` | string | no | Target database |
Returns `{ rows: [...], row_count: N }`.
---
### `explain_postgres`
Run `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` on a query and return the execution plan.
| Parameter | Type | Required | Description |
|--------------|--------|----------|-------------|
| `sql` | string | yes | SELECT query to analyse (do not include `EXPLAIN`) |
| `project_id` | string | no | Target database |
Returns `{ plan: [...] }`. Key things to check in the plan:
- **Seq Scan on a large table** → missing index on the `WHERE` column
- **Actual rows ≫ Plan rows** → stale statistics; run `ANALYZE <table>`
- **High shared_blks_read** → I/O-bound; working set does not fit in `shared_buffers`
---
### `pg_stat_statements`
Return the top N query patterns ranked by total cumulative execution time.
| Parameter | Type | Default | Description |
|--------------|---------|---------|-------------|
| `limit` | integer | 20 | Number of queries to return (max 100) |
| `project_id` | string | no | Target database |
Returns rows with `total_exec_sec`, `mean_exec_sec`, `max_exec_sec`, `calls`, `shared_blks_hit`, `shared_blks_read`.
Requires the `pg_stat_statements` extension. Enabled by default on AWS RDS, GCP Cloud SQL, and Supabase. On self-hosted Postgres: `CREATE EXTENSION pg_stat_statements;`
---
## Recommended workflows
**Explore an unknown database**
```
describe_postgres_schema → execute_postgres
```
**Debug a slow query**
```
describe_postgres_schema (check what indexes exist)
→ explain_postgres (verify the planner uses them)
→ execute_postgres (run once plan looks correct)
```
**Performance audit**
```
pg_stat_statements (find the most expensive patterns)
→ explain_postgres (drill into the worst offender)
→ describe_postgres_schema (check if a missing index would help)
```
---
## Environment variables
| Variable | Default | Description |
|---------------------------------|------------------|-------------|
| `DATABASES_FILE` | `databases.json` | Path to the databases registry file |
| `DEFAULT_PROJECT` | `default` | Fallback project when `project_id` is omitted |
| `LOG_LEVEL` | `INFO` | Python logging level |
| `CONNECT_TIMEOUT` | `10.0` | asyncpg connection timeout in seconds |
| `EXPLAIN_TIMEOUT_MS` | `30000` | Statement timeout for EXPLAIN ANALYZE in milliseconds |
| `STAT_STATEMENTS_DEFAULT_LIMIT` | `20` | Default row limit for `pg_stat_statements` |
TDQS
A4.4/5.0
Scored across 5 tools
Disambiguation5/5
Each tool has a clearly distinct purpose: schema inspection, query execution, explain planning, project listing, and performance analysis. No overlap exists.
Naming Consistency4/5
Tools follow a mostly consistent verb_noun pattern (describe_, execute_, explain_, list_), except pg_stat_statements which is a fixed PostgreSQL function name. Overall, naming is clear and predictable.
Tool Count5/5
Five tools is ideal for a query executor, covering key operations without unnecessary overhead. Each tool serves a specific need.
Completeness5/5
The tool set covers essential operations: schema inspection, query execution with read-only/write modes, explain analysis, project management, and performance monitoring. No obvious gaps for the domain.
Maintenance
ActivityInactive
ResponsivenessNo issues