Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
46321f1
Update README for WSL UVX usage
valdezm May 14, 2025
b54c5da
Update README for WSL UVX usage, no formatting
valdezm May 14, 2025
a524b92
more info, clearer
valdezm May 14, 2025
caa0c88
edited the instructions
jssmith May 15, 2025
2608133
Return error message if table does not exist
Promises Jan 30, 2026
5261095
docs: Add AGENTS.md and CLAUDE.md for AI agent contribution guidance
Dev-iL Mar 19, 2026
161d899
feat: add schema comments and materialized view support to metadata t…
stephanhaeuslschmid Mar 20, 2026
f9dfd62
fix: add obj_description and col_description to allowed functions
stephanhaeuslschmid Mar 20, 2026
050adc8
fix(security): add SQL injection prevention to ExplainPlanTool
RajatRaiD11 Mar 20, 2026
67fd794
test: add comprehensive tests for _validate_explain_input
RajatRaiD11 Mar 20, 2026
d7297c1
feat: improve tool descriptions for LLM discoverability
stephanhaeuslschmid Mar 20, 2026
fd0f099
refactor: Modernize type hints to use Python 3.12 built-in generics
Brian-kipkoech-mutai May 11, 2026
74a8f18
Connect to database lazily with configurable idle timeout
Jun 17, 2026
05c2d0f
fix: stop pool churn and pool leaks on query errors (#98)
eculver Aug 1, 2026
2932bfe
chore: bump Python deps and Debian base image past known CVEs (#2)
eculver Aug 5, 2026
a8e4a08
Merge branch 'main' into fix/pool-invalidation-churn-and-leak
eculver Aug 18, 2026
7fac095
Add timezone to ALLOWED_FUNCTIONS
jack-r-warren Aug 19, 2026
a3f5f1a
fix: Reset pooled connections to drop leaked session GUCs and HypoPG …
Xxxgy0933 Aug 27, 2026
3d84b93
Merge PR #74
cupskeee Sep 6, 2026
72db260
Merge PR #158
cupskeee Sep 6, 2026
80de49a
Merge PR #173
cupskeee Sep 6, 2026
b252669
Merge PR #209
cupskeee Sep 6, 2026
5d7e798
Merge PR #161
cupskeee Sep 6, 2026
2fc74b2
Merge PR #197
cupskeee Sep 6, 2026
e8df381
Merge PR #143
cupskeee Sep 6, 2026
e31a0fc
Merge PR #195
cupskeee Sep 6, 2026
739db91
Merge PR #160 (combine matview/comment support with #143 not-exist er…
cupskeee Sep 6, 2026
8557c64
Merge PR #180 (combine lazy/idle pool with #195 error-classification …
cupskeee Sep 6, 2026
d7cd6d2
Merge PR #213 (add connection-reset callback alongside #195/#180 pool…
cupskeee Sep 6, 2026
b46e659
fix(post-merge): reconcile cross-PR coherence (typing modernization +…
cupskeee Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# AGENTS.md — postgres-mcp

Universal instructions for AI agents contributing to this project.

**Scope:** Development conventions, CI commands, and safety rules for the postgres-mcp codebase. Does NOT cover how to use the MCP tools as an end user (see `.claude/skills/postgres-mcp-usage.md` for that).

## Safety Guardrails

**Hard rule: every new tool MUST call `get_sql_driver()` to obtain its SQL driver.** This is the only mechanism that enforces the server's access mode (UNRESTRICTED vs RESTRICTED). Bypassing it — by instantiating `SqlDriver` directly — silently disables safety protections.

- **UNRESTRICTED mode** — full read/write SQL access. Intended for development.
- **RESTRICTED mode** — read-only transactions via `SafeSqlDriver`, 30-second query timeout. Intended for production.
- `get_sql_driver()` returns a `SqlDriver` or `SafeSqlDriver` depending on `current_access_mode`.
- `execute_sql` is dynamically registered with different MCP annotations per mode. Follow this pattern for any new tool whose behavior changes with access mode.

## CI Commands

Run these locally before pushing. They mirror `.github/workflows/build.yml`:

```bash
uv sync # install dependencies
uv run ruff format --check . # format check
uv run ruff check . # lint
uv run pyright # type check
uv run pytest -v --log-cli-level=INFO # tests (unit + integration)
```

All five must pass for CI to be green.

## Project Structure

```
src/postgres_mcp/
server.py # MCP server entry point, tool definitions (~700 lines)
sql/ # SQL driver, SafeSqlDriver, parameterized queries, extensions
index/ # Index tuning (DTA algorithm, LLM optimizer)
explain/ # EXPLAIN plan analysis
database_health/ # Health checks (index, connection, vacuum, sequence, replication, buffer, constraint)
top_queries/ # pg_stat_statements query analysis
artifacts.py # Response types (ExplainPlanArtifact, ErrorResult)

tests/
unit/ # Fast, no database required
integration/ # Requires a running PostgreSQL instance (Docker)
conftest.py # Shared fixtures
```

Key entry point: `server.py` — contains all `@mcp.tool` definitions and the `main()` function.

## Code Conventions

See `pyproject.toml` for full ruff/pyright configuration. Key points:

- **Async throughout** — all tool handlers and SQL operations are `async`. Use `await` for database calls.
- **Line length:** 150 characters.
- **Quotes:** double quotes (ruff format).
- **Imports:** force-single-line (`from x import y`, one per line). Enforced by ruff isort.
- **Type checking:** pyright in standard mode, Python 3.12. Ruff lint targets Python 3.9 for compatibility.
- **Lint rules:** E, F, I, B, W, N, UP, RUF. See `pyproject.toml [tool.ruff]` for active ignores.

### Patterns to Follow

- Use `Field(description=..., default=...)` from pydantic for tool parameter definitions.
- Return `ResponseType` (list of `TextContent | ImageContent | EmbeddedResource`) from tool handlers.
- Use `format_text_response()` and `format_error_response()` helpers in server.py.
- Parameterized queries: use `SafeSqlDriver.execute_param_query(driver, sql, params)` with `{}` placeholders (not `%s` or `$1`).

## Testing

- **Unit tests** (`tests/unit/`): test logic without a database. Mock SQL drivers as needed.
- **Integration tests** (`tests/integration/`): run against a real PostgreSQL instance via Docker. See `tests/Dockerfile.postgres-hypopg` for the test image with pg_stat_statements and HypoPG pre-installed.
- Test runner: `uv run pytest -v --log-cli-level=INFO`
- Async tests use `pytest-asyncio`.

## Dependencies

- **Package manager:** `uv` (not pip/pipx for development).
- **Build system:** hatchling.
- **Key libraries:** FastMCP (`mcp` package), psycopg3 (async PostgreSQL), pglast (SQL parsing for safety), pydantic (validation), instructor (LLM integration).
- Add dev dependencies to `[dependency-groups] dev` in `pyproject.toml`.
60 changes: 60 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# CLAUDE.md — postgres-mcp

Claude-specific instructions for contributing to this project. Read AGENTS.md first for shared rules (CI commands, safety guardrails, code conventions).

**Scope:** Claude Code tool preferences, MCP-aware development workflows, and commit conventions. Does NOT duplicate content from AGENTS.md.

## Tool Preferences

Prefer Claude Code built-in tools over shell equivalents:

| Prefer | Over |
|--------|------|
| `Read` | `cat`, `head`, `tail` |
| `Edit` | `sed`, `awk` |
| `Grep` | `grep`, `rg` |
| `Glob` | `find`, `ls` for file search |
| `Write` | `echo >`, heredoc |

Use `Bash` only for commands that require shell execution (git, uv, pytest, etc.).

## MCP-Aware Development

When connected to a postgres-mcp instance (tools visible in MCP tool list):

- **Use MCP tools to validate changes.** After modifying a tool handler, call the tool through MCP to verify it works. For example, after editing `list_schemas`, call `list_schemas` via MCP to confirm the output.
- **Test access mode behavior.** If your change touches SQL execution paths, verify behavior in both UNRESTRICTED and RESTRICTED modes.
- **Use `execute_sql` to inspect schema** when writing tests or debugging query-related code.

When NOT connected to an MCP instance:

- Run unit tests: `uv run pytest tests/unit/ -v`
- For integration tests, start the Docker test database first (see `tests/Dockerfile.postgres-hypopg`).
- Review SQL strings manually — the MCP tools won't be available for live validation.

## Commit Style

This project uses a mixed commit style. Follow these conventions:

- **Prefix with type when applicable:** `feat:`, `fix:`, `refactor:`, `chore:`, `docs:` — lowercase, no scope.
- **Imperative mood for the subject line.** Example: `fix: Support PostgreSQL 12 in get_top_queries`
- **Include PR number** if merging via GitHub: `Add streamable HTTP transport support (#134)`
- **Keep subject line under 72 characters.**
- No body required for small changes. Add a body for non-obvious context.

## Development Workflow

1. Read the relevant source files before making changes. Start with `server.py` for tool definitions.
2. Run the full CI suite before considering work complete (see AGENTS.md for commands).
3. If adding a new MCP tool:
- Define it in `server.py` with `@mcp.tool` decorator.
- Use `get_sql_driver()` — this is a hard safety rule (see AGENTS.md).
- Add unit tests in `tests/unit/` and integration tests in `tests/integration/`.

## Project-Specific Context

- The codebase is async throughout (psycopg3 async, FastMCP).
- `execute_sql` is dynamically registered (not decorated) — see `main()` in server.py.
- Index tuning uses the DTA algorithm by default. LLM optimization (`method="llm"`) is experimental and requires `OPENAI_API_KEY`.
- SQL safety parsing uses `pglast` to reject COMMIT/ROLLBACK in restricted mode.
- Parameterized queries use `{}` placeholders via `SafeSqlDriver.execute_param_query()`, not `%s` or `$1`.
11 changes: 8 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# First, build the application in the `/app` directory.
# See `Dockerfile` for details.
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
FROM ghcr.io/astral-sh/uv:python3.12-trixie-slim AS builder
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy

# Disable Python downloads, because we want to use the system interpreter
Expand All @@ -22,9 +22,9 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev


FROM python:3.12-slim-bookworm
FROM python:3.12-slim-trixie
# It is important to use the image that matches the builder, as the path to the
# Python executable must be the same, e.g., using `python:3.11-slim-bookworm`
# Python executable must be the same, e.g., using `python:3.11-slim-trixie`
# will fail.

RUN groupadd -r app && useradd -r -g app app
Expand All @@ -49,6 +49,11 @@ RUN apt-get update && apt-get install -y \
net-tools \
&& rm -rf /var/lib/apt/lists/*

# pip is unused at runtime (the venv is prebuilt) but the base image's copy
# carries known CVEs. PATH resolves `python` to the pip-less venv interpreter,
# so name the system one.
RUN /usr/local/bin/python -m pip install --no-cache-dir --upgrade pip

COPY docker-entrypoint.sh /app/
RUN chmod +x /app/docker-entrypoint.sh

Expand Down
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,30 @@ The Postgres MCP Pro Docker image will automatically remap the hostname `localho
}
```

##### If you are using `uvx` with Windows WSL

If you are using Windows WSL, you can use `uvx` with the following configuration to download and run Postgres MCP Pro:

```json
{
"mcpServers": {
"postgres": {
"command": "wsl.exe",
"args": [
"bash",
"-c",
"/home/$WSL_USER/.local/bin/uvx --from git+https://github.com/crystaldba/postgres-mcp@main postgres-mcp postgresql://username:password@localhost:5432/dbname --access-mode=unrestricted"
]
}
}
}
```

Notes:
- This command runs the latest version of Postgres MCP Pro from the `main` branch.
- Replace `/home/$WSL_USER/.local/bin/uvx` with the path to your `uvx` command. If you do not know where `uvx` is installed, run `which uvx` in WSL.
- If you do not have `uvx` installed, you may install it using `pip`.


##### Connection URI

Expand All @@ -232,6 +256,42 @@ Restricted mode is the default. To allow write operations, add `--access-mode=un
> prints a warning explaining the prompt-injection risk.


##### Idle Connection Timeout

Postgres MCP Pro connects to the database **lazily** — no connection is opened when the server starts, only on the first tool call that needs the database. Once a connection has been idle for a while it is reaped, so a server you configure but don't use holds zero Postgres connections. This is helpful when you define many database configs and don't want every server holding an open connection from session start.

The idle timeout defaults to **300 seconds** and is configurable with the `--max-idle` flag (in seconds), or the `DATABASE_MAX_IDLE` environment variable. The flag takes precedence over the environment variable, and invalid values (non-numeric, zero, or negative) fall back to the default.

```json
{
"mcpServers": {
"postgres": {
"command": "uvx",
"args": [
"postgres-mcp",
"--access-mode=unrestricted",
"--max-idle=120"
],
"env": {
"DATABASE_URI": "postgresql://username:password@localhost:5432/dbname"
}
}
}
}
```

Equivalently, using the environment variable instead of the flag:

```json
"env": {
"DATABASE_URI": "postgresql://username:password@localhost:5432/dbname",
"DATABASE_MAX_IDLE": "120"
}
```

A reaped connection is re-established transparently on the next tool call, restarting the idle timer. Lower values release connections faster (good for many-database setups); higher values keep connections warm to avoid reconnect latency on frequently-used databases.


#### Other MCP Clients

Many MCP clients have similar configuration files to Claude Desktop, and you can adapt the examples above to work with the client of your choice.
Expand Down
4 changes: 0 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,7 @@ lint.select = [
"RUF" # ruff-specific rules
]

# TODO: Remove these ignores when fixing #129 (code modernization)
lint.ignore = [
"UP006", # Use `list` instead of `List` for type annotations
"UP035", # Import from `collections.abc` instead of `typing`
"UP045", # Use `X | None` instead of `Optional[X]`
"RUF059", # Unused unpacked variable
"RUF100", # Unused noqa directive
]
Expand Down
3 changes: 1 addition & 2 deletions src/postgres_mcp/database_health/database_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import logging
from enum import Enum
from typing import List

import mcp.types as types

Expand All @@ -14,7 +13,7 @@
from .sequence_health_calc import SequenceHealthCalc
from .vacuum_health_calc import VacuumHealthCalc

ResponseType = List[types.TextContent | types.ImageContent | types.EmbeddedResource]
ResponseType = list[types.TextContent | types.ImageContent | types.EmbeddedResource]

logger = logging.getLogger(__name__)

Expand Down
7 changes: 3 additions & 4 deletions src/postgres_mcp/database_health/replication_calc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from dataclasses import dataclass
from typing import Optional

from ..sql import SqlDriver

Expand All @@ -14,15 +13,15 @@ class ReplicationSlot:
@dataclass
class ReplicationMetrics:
is_replica: bool
replication_lag_seconds: Optional[float]
replication_lag_seconds: float | None
is_replicating: bool
replication_slots: list[ReplicationSlot]


class ReplicationCalc:
def __init__(self, sql_driver: SqlDriver):
self.sql_driver = sql_driver
self._server_version: Optional[int] = None
self._server_version: int | None = None
self._feature_support: dict[str, bool] = {}

async def replication_health_check(self) -> str:
Expand Down Expand Up @@ -85,7 +84,7 @@ async def _is_replica(self) -> bool:
result_list = [dict(x.cells) for x in result] if result is not None else []
return bool(result_list[0]["pg_is_in_recovery"]) if result_list else False

async def _get_replication_lag(self) -> Optional[float]:
async def _get_replication_lag(self) -> float | None:
"""Get replication lag in seconds."""
if not self._feature_supported("replication_lag"):
return None
Expand Down
Loading