Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b9f3208
Add percentile_cont, percentile_disc, and mode to ALLOWED_FUNCTIONS (…
MehdiZonjy Jan 19, 2026
dd7f934
Fix up some docker build/running issues (#110)
stbenjam Jan 19, 2026
4cd8d44
Fix escaping sequence names in health check (#128)
jssmith Jan 19, 2026
84916ef
feat: Add tool annotations for improved LLM tool understanding (#122)
bryankthompson Jan 19, 2026
cc52408
chore: Update all dependencies to latest versions (#130)
jssmith Jan 19, 2026
f34567b
Fix broken links to Reference Postgres MCP Server (#131)
jssmith Jan 19, 2026
509be60
Update README.md for uvx instructions (#100)
tybalex Jan 20, 2026
3ad5cff
fix: Support PostgreSQL 12 in get_top_queries (#132)
jssmith Jan 20, 2026
dda43e5
add instructions for MCP server in Qodo Gen (#81)
InTheCloudDan Jan 20, 2026
d03cf9f
refactor: Extract pg_stat_statements column logic to helper (#133)
jssmith Jan 20, 2026
07eb329
Add streamable HTTP transport support (#134)
jssmith Jan 22, 2026
e6c04a6
add mcp resources
DYL521 Dec 18, 2025
6d11a77
add db_connections_cache
DYL521 Dec 19, 2025
3ea2663
add tabf and build to ecr
daochidq Dec 19, 2025
cea81be
fix docker path
daochidq Dec 19, 2025
2f098a1
reformat
daochidq Dec 19, 2025
d8a0239
Add dynamically_register_resources and remove list_schemas and list_o…
DYL521 Dec 23, 2025
655f2d7
add local test env for local dev
Dec 26, 2025
4f87dcd
reduce the code for register resource template
Dec 26, 2025
fce7812
If password was encoded, it would double encode. Switched to only enc…
caleb-mabry Jan 7, 2026
5557997
Replaced import path
caleb-mabry Jan 7, 2026
1807f31
Linting fix
caleb-mabry Jan 7, 2026
3ca7dc6
Add additional test cases
caleb-mabry Jan 7, 2026
14f7064
Resolve formatting
caleb-mabry Jan 7, 2026
04483ee
Lint fix
caleb-mabry Jan 7, 2026
98281fb
Merge branch 'main' into sync-fork
caleb-mabry Feb 10, 2026
b138e31
Lint
caleb-mabry Feb 10, 2026
f586abb
Change the import
caleb-mabry Feb 10, 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
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ FROM python:3.12-slim-bookworm
# Python executable must be the same, e.g., using `python:3.11-slim-bookworm`
# will fail.

RUN groupadd -r app && useradd -r -g app app

COPY --from=builder --chown=app:app /app /app

ENV PATH="/app/.venv/bin:$PATH"
Expand All @@ -50,6 +52,8 @@ RUN apt-get update && apt-get install -y \
COPY docker-entrypoint.sh /app/
RUN chmod +x /app/docker-entrypoint.sh

USER app

# Expose the SSE port
EXPOSE 8000

Expand Down
30 changes: 25 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,25 @@ The Postgres MCP Pro Docker image will automatically remap the hostname `localho
- MacOS/Windows: Uses `host.docker.internal` automatically
- Linux: Uses `172.17.0.1` or the appropriate host address automatically

##### If you are using `uvx`

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


##### If you are using `pipx`

Expand Down Expand Up @@ -215,6 +234,7 @@ Many MCP clients have similar configuration files to Claude Desktop, and you can
- If you are using Cursor, you can use navigate from the `Command Palette` to `Cursor Settings`, then open the `MCP` tab to access the configuration file.
- If you are using Windsurf, you can navigate to from the `Command Palette` to `Open Windsurf Settings Page` to access the configuration file.
- If you are using Goose run `goose configure`, then select `Add Extension`.
- If you are using Qodo Gen, open the Chat panel, click `Connect more tools`, click `+ Add new MCP`, then add the new configuration.

## SSE Transport

Expand Down Expand Up @@ -313,7 +333,7 @@ The [MCP standard](https://modelcontextprotocol.io/) defines various types of en

Postgres MCP Pro provides functionality via [MCP tools](https://modelcontextprotocol.io/docs/concepts/tools) alone.
We chose this approach because the [MCP client ecosystem](https://modelcontextprotocol.io/clients) has widespread support for MCP tools.
This contrasts with the approach of other Postgres MCP servers, including the [Reference Postgres MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/postgres), which use [MCP resources](https://modelcontextprotocol.io/docs/concepts/resources) to expose schema information.
This contrasts with the approach of other Postgres MCP servers, including the [Reference Postgres MCP Server](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/postgres), which use [MCP resources](https://modelcontextprotocol.io/docs/concepts/resources) to expose schema information.


Postgres MCP Pro Tools:
Expand All @@ -336,7 +356,7 @@ Postgres MCP Pro Tools:
**Postgres MCP Servers**
- [Query MCP](https://github.com/alexander-zuev/supabase-mcp-server). An MCP server for Supabase Postgres with a three-tier safety architecture and Supabase management API support.
- [PG-MCP](https://github.com/stuzero/pg-mcp-server). An MCP server for PostgreSQL with flexible connection options, explain plans, extension context, and more.
- [Reference PostgreSQL MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/postgres). A simple MCP Server implementation exposing schema information as MCP resources and executing read-only queries.
- [Reference PostgreSQL MCP Server](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/postgres). A simple MCP Server implementation exposing schema information as MCP resources and executing read-only queries.
- [Supabase Postgres MCP Server](https://github.com/supabase-community/supabase-mcp). This MCP Server provides Supabase management features and is actively maintained by the Supabase community.
- [Nile MCP Server](https://github.com/niledatabase/nile-mcp-server). An MCP server providing access to the management API for the Nile's multi-tenant Postgres service.
- [Neon MCP Server](https://github.com/neondatabase-labs/mcp-server-neon). An MCP server providing access to the management API for Neon's serverless Postgres service.
Expand Down Expand Up @@ -524,7 +544,7 @@ We remain open to revising this decision in the future.

### Connection Configuration

Like the [Reference PostgreSQL MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/postgres), Postgres MCP Pro takes Postgres connection information at startup.
Like the [Reference PostgreSQL MCP Server](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/postgres), Postgres MCP Pro takes Postgres connection information at startup.
This is convenient for users who always connect to the same database but can be cumbersome when users switch databases.

An alternative approach, taken by [PG-MCP](https://github.com/stuzero/pg-mcp-server), is provide connection details via MCP tool calls at the time of use.
Expand All @@ -549,7 +569,7 @@ However, we do not know whether other LLMs do so as reliably and capably.

*Would it be better to provide schema information using [MCP resources](https://modelcontextprotocol.io/docs/concepts/resources) rather than [MCP tools](https://modelcontextprotocol.io/docs/concepts/tools)?*

The [Reference PostgreSQL MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/postgres) uses resources to expose schema information rather than tools.
The [Reference PostgreSQL MCP Server](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/postgres) uses resources to expose schema information rather than tools.
Navigating resources is similar to navigating a file system, so this approach is natural in many ways.
However, resource support is less widespread than tool support in the MCP client ecosystem (see [example clients](https://modelcontextprotocol.io/clients)).
In addition, while the MCP standard says that resources can be accessed by either AI agents or end-user humans, some clients only support human navigation of the resource tree.
Expand All @@ -570,7 +590,7 @@ While this is a good approach, many find this cumbersome in practice.
Postgres does not provide a way to place a connection or session into read-only mode, so Postgres MCP Pro uses a more complex approach to ensure read-only SQL execution on top of a read-write connection.

Postgres MCP Provides a read-only transaction mode that prevents data and schema modifications.
Like the [Reference PostgreSQL MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/postgres), we use read-only transactions to provide protected SQL execution.
Like the [Reference PostgreSQL MCP Server](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/postgres), we use read-only transactions to provide protected SQL execution.

To make this mechanism robust, we need to ensure that the SQL does not somehow circumvent the read-only transaction mode, say by issuing a `COMMIT` or `ROLLBACK` statement and then beginning a new transaction.

Expand Down
16 changes: 3 additions & 13 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,6 @@ echo "${processed_args[@]}" >&2
echo "----------------" >&2

# Execute the command with the processed arguments
"${processed_args[@]}"

# Capture exit code from the Python process
exit_code=$?

# If the Python process failed, print additional debug info
if [ $exit_code -ne 0 ]; then
echo "ERROR: Command failed with exit code $exit_code" >&2
echo "Command was: ${processed_args[@]}" >&2
fi

# Return the exit code from the Python process
exit $exit_code
# Use exec to replace the shell with the Python process, making it PID 1
# This ensures signals (SIGTERM, SIGINT) are properly received
exec "${processed_args[@]}"
34 changes: 21 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ description = "PostgreSQL Tuning and Analysis Tool"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"mcp[cli]>=1.5.0",
"psycopg[binary]>=3.2.6",
"humanize>=4.8.0",
"pglast==7.2.0",
"attrs>=25.3.0",
"psycopg-pool>=3.2.6",
"instructor>=1.7.9",
"mcp[cli]>=1.25.0",
"psycopg[binary]>=3.3.2",
"humanize>=4.15.0",
"pglast==7.11",
"attrs>=25.4.0",
"psycopg-pool>=3.3.0",
"instructor>=1.14.4",
]
license = "mit"
license-files = ["LICENSE"]
Expand All @@ -38,18 +38,18 @@ asyncio_default_fixture_loop_scope = "function"
[dependency-groups]
dev = [
"docker>=7.1.0",
"pyright==1.1.398",
"pytest-asyncio>=0.26.0",
"pytest>=8.3.5",
"ruff==0.11.2",
"pyright==1.1.408",
"pytest-asyncio>=1.3.0",
"pytest>=9.0.2",
"ruff==0.14.13",
]

[tool.black]
line-length = 150

[tool.ruff]
line-length = 150
target-version = "py38"
target-version = "py39"
exclude = [".venv*"]

lint.select = [
Expand All @@ -65,6 +65,15 @@ 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
]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
Expand All @@ -80,7 +89,6 @@ known-first-party = ["postgres-mcp"]
[tool.pyright]
typeCheckingMode = "standard"
pythonVersion = "3.12"
strictParameterNullChecking = true
reportMissingTypeStubs = false
# reportUnknownMemberType = true
# reportUnknownParameterType = true
Expand Down
29 changes: 21 additions & 8 deletions src/postgres_mcp/database_health/sequence_health_calc.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from dataclasses import dataclass

from psycopg.sql import Identifier
Expand Down Expand Up @@ -135,14 +136,26 @@ async def _get_sequence_metrics(self) -> list[SequenceMetrics]:
return sequence_metrics

def _parse_sequence_name(self, default_value: str) -> tuple[str, str]:
"""Parse schema and sequence name from default value expression."""
# Handle both formats:
# nextval('id_seq'::regclass)
# nextval(('id_seq'::text)::regclass)

# Remove nextval and cast parts
clean_value = default_value.replace("nextval('", "").replace("'::regclass)", "")
clean_value = clean_value.replace("('", "").replace("'::text)", "")
"""Parse schema and sequence name from default value expression.

Handles formats like:
- nextval('id_seq'::regclass)
- nextval(('id_seq'::text)::regclass)
- nextval('"UpperCaseSeq"'::regclass)
- nextval('"Schema"."Seq"'::regclass)

Note: Sequence names containing literal dots (e.g., "my.seq") are not
supported and will be incorrectly parsed as schema.name.
"""
# Extract the sequence reference from inside the single quotes
# Handles both nextval('...') and nextval(('...'::text)::regclass)
match = re.search(r"nextval\(\(?'([^']+)'", default_value)
if not match:
return "public", ""

clean_value = match.group(1)
# Remove quotes so sql.Identifier can add them correctly
clean_value = clean_value.replace('"', "")

# Split into schema and sequence
parts = clean_value.split(".")
Expand Down
67 changes: 58 additions & 9 deletions src/postgres_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import mcp.types as types
from mcp.server.fastmcp import FastMCP
from mcp.types import ToolAnnotations
from pydantic import Field
from pydantic import validate_call

Expand Down Expand Up @@ -181,7 +182,13 @@ async def get_object_details(
return format_error_response(str(e))


@mcp.tool(description="Explains the execution plan for a SQL query, showing how the database will execute it and provides detailed cost estimates.")
@mcp.tool(
description="Explains the execution plan for a SQL query, showing how the database will execute it and provides detailed cost estimates.",
annotations=ToolAnnotations(
title="Explain Query",
readOnlyHint=True,
),
)
async def explain_query(
sql: str = Field(description="SQL query to explain"),
analyze: bool = Field(
Expand Down Expand Up @@ -276,7 +283,13 @@ async def execute_sql(
return format_error_response(str(e))


@mcp.tool(description="Analyze frequently executed queries in the database and recommend optimal indexes")
@mcp.tool(
description="Analyze frequently executed queries in the database and recommend optimal indexes",
annotations=ToolAnnotations(
title="Analyze Workload Indexes",
readOnlyHint=True,
),
)
@validate_call
async def analyze_workload_indexes(
max_index_size_mb: int = Field(description="Max index size in MB", default=10000),
Expand All @@ -297,7 +310,13 @@ async def analyze_workload_indexes(
return format_error_response(str(e))


@mcp.tool(description="Analyze a list of (up to 10) SQL queries and recommend optimal indexes")
@mcp.tool(
description="Analyze a list of (up to 10) SQL queries and recommend optimal indexes",
annotations=ToolAnnotations(
title="Analyze Query Indexes",
readOnlyHint=True,
),
)
@validate_call
async def analyze_query_indexes(
queries: list[str] = Field(description="List of Query strings to analyze"),
Expand Down Expand Up @@ -334,7 +353,11 @@ async def analyze_query_indexes(
"- buffer - checks for buffer cache hit rates for indexes and tables\n"
"- constraint - checks for invalid constraints\n"
"- all - runs all checks\n"
"You can optionally specify a single health check or a comma-separated list of health checks. The default is 'all' checks."
"You can optionally specify a single health check or a comma-separated list of health checks. The default is 'all' checks.",
annotations=ToolAnnotations(
title="Analyze Database Health",
readOnlyHint=True,
),
)
async def analyze_db_health(
health_type: str = Field(
Expand All @@ -356,6 +379,10 @@ async def analyze_db_health(
@mcp.tool(
name="get_top_queries",
description=f"Reports the slowest or most resource-intensive queries using data from the '{PG_STAT_STATEMENTS}' extension.",
annotations=ToolAnnotations(
title="Get Top Queries",
readOnlyHint=True,
),
)
async def get_top_queries(
sort_by: str = Field(
Expand Down Expand Up @@ -397,9 +424,9 @@ async def main():
parser.add_argument(
"--transport",
type=str,
choices=["stdio", "sse"],
choices=["stdio", "sse", "streamable-http"],
default="stdio",
help="Select MCP transport: stdio (default) or sse",
help="Select MCP transport: stdio (default), sse, or streamable-http",
)
parser.add_argument(
"--sse-host",
Expand All @@ -413,6 +440,18 @@ async def main():
default=8000,
help="Port for SSE server (default: 8000)",
)
parser.add_argument(
"--streamable-http-host",
type=str,
default="localhost",
help="Host to bind streamable HTTP server to (default: localhost)",
)
parser.add_argument(
"--streamable-http-port",
type=int,
default=8000,
help="Port for streamable HTTP server (default: 8000)",
)

args = parser.parse_args()

Expand All @@ -423,7 +462,14 @@ async def main():
if sql_driver_module.current_access_mode == AccessMode.UNRESTRICTED:
mcp.add_tool(execute_sql, description="Execute any SQL query")
else:
mcp.add_tool(execute_sql, description="Execute a read-only SQL query")
mcp.add_tool(
execute_sql,
description="Execute a read-only SQL query",
annotations=ToolAnnotations(
title="Execute SQL (Read-Only)",
readOnlyHint=True,
),
)

logger.info(f"Starting PostgreSQL MCP Server in {sql_driver_module.current_access_mode.upper()} mode")

Expand Down Expand Up @@ -469,11 +515,14 @@ async def main():
# Run the server with the selected transport (always async)
if args.transport == "stdio":
await mcp.run_stdio_async()
else:
# Update FastMCP settings based on command line arguments
elif args.transport == "sse":
mcp.settings.host = args.sse_host
mcp.settings.port = args.sse_port
await mcp.run_sse_async()
elif args.transport == "streamable-http":
mcp.settings.host = args.streamable_http_host
mcp.settings.port = args.streamable_http_port
await mcp.run_streamable_http_async()


async def shutdown(sig=None):
Expand Down
3 changes: 3 additions & 0 deletions src/postgres_mcp/sql/safe_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ class SafeSqlDriver(SqlDriver):
"trim_scale",
"trunc",
"width_bucket",
"percentile_cont",
"percentile_disc",
"mode",
# Trigonometric functions
"acos",
"acosd",
Expand Down
Loading