A production-ready MCP server that exposes PostgreSQL databases via the Model Context Protocol
Project description
mcp-postgres
A production-ready Model Context Protocol (MCP) server that gives AI assistants (Claude, etc.) direct, safe access to your PostgreSQL database.
What it does
mcp-postgres exposes five tools to any MCP-compatible client:
| Tool | Description |
|---|---|
query |
Execute a read-only SELECT statement and get JSON results |
execute |
Run a write statement (INSERT, UPDATE, DELETE, DDL) — requires ALLOW_WRITE=true |
list_schemas |
List all user schemas in the database |
list_tables |
List tables/views in a schema with row estimates and sizes |
describe_table |
Get columns, primary key, foreign keys, and indexes for a table |
Quickstart
1. Install
pip install mcp-postgres
Or install from source:
git clone https://github.com/madmarin/mcp-postgres
cd mcp-postgres
pip install -e .
2. Configure
Copy .env.example to .env and fill in your connection details:
cp .env.example .env
Minimum required:
DATABASE_URL=postgresql+psycopg://user:password@localhost:5432/mydb
3. Run
mcp-postgres
The server starts in stdio mode by default, ready to be used by any MCP client.
Add to Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"postgres": {
"command": "mcp-postgres",
"env": {
"DATABASE_URL": "postgresql+psycopg://user:password@localhost:5432/mydb"
}
}
}
}
Restart Claude Desktop — you will see the PostgreSQL tools available.
Add to Claude Code (CLI)
claude mcp add postgres -- mcp-postgres
Then set the environment variable:
export DATABASE_URL="postgresql+psycopg://user:password@localhost:5432/mydb"
Configuration Reference
All settings are read from environment variables or a .env file in the working directory.
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
— | Full connection string (postgresql+psycopg://...). Takes priority over individual fields. |
POSTGRES_HOST |
localhost |
Host (used if DATABASE_URL is not set) |
POSTGRES_PORT |
5432 |
Port |
POSTGRES_DB |
postgres |
Database name |
POSTGRES_USER |
postgres |
Username |
POSTGRES_PASSWORD |
— | Password |
MCP_SERVER_NAME |
mcp-postgres |
Name reported to MCP clients |
MCP_TRANSPORT |
stdio |
Transport: stdio or sse |
LOG_LEVEL |
INFO |
DEBUG, INFO, WARNING, ERROR |
ALLOW_WRITE |
false |
Set to true to enable the execute tool |
POOL_MIN_SIZE |
2 |
Minimum connections in pool |
POOL_MAX_SIZE |
10 |
Maximum connections in pool |
QUERY_TIMEOUT |
30.0 |
Per-statement timeout in seconds |
Tool Reference
query(sql, params?)
Execute a read-only SELECT statement.
Input:
sql — SELECT statement or CTE
params — optional list of values for parameterized queries
Output (JSON):
{
"columns": ["id", "name", "age"],
"rows": [[1, "Alice", 30], [2, "Bob", 25]],
"row_count": 2,
"execution_time_ms": 3.14
}
Always use params for user-supplied values — never interpolate them into the SQL string.
# Safe
query("SELECT * FROM users WHERE name = %s", ["Alice"])
# Never do this
query(f"SELECT * FROM users WHERE name = '{user_input}'")
execute(sql, params?)
Run a write statement. Requires ALLOW_WRITE=true.
Output (JSON):
{
"rows_affected": 1,
"status": "INSERT 0 1",
"execution_time_ms": 2.5
}
list_schemas()
Output (JSON):
[
{"schema_name": "public", "owner": "postgres"},
{"schema_name": "analytics", "owner": "alice"}
]
list_tables(schema?)
Output (JSON):
[
{
"table_name": "users",
"table_type": "BASE TABLE",
"row_estimate": 12345,
"total_size": "2048 kB"
}
]
describe_table(table, schema?)
Output (JSON):
{
"table": "users",
"schema": "public",
"columns": [
{"name": "id", "type": "integer", "nullable": false, "default": "nextval(...)"},
{"name": "name", "type": "text", "nullable": false, "default": null}
],
"primary_key": ["id"],
"foreign_keys": [],
"indexes": [
{"name": "users_pkey", "definition": "CREATE UNIQUE INDEX ..."}
]
}
Security Model
- Read-only by default: the
executetool is disabled unless you explicitly setALLOW_WRITE=true. This prevents accidental mutations. - Parameterized queries: all tools use psycopg's parameterized query API. SQL is never built by string concatenation.
- Denylist: even with
ALLOW_WRITE=true, certain destructive patterns (DROP DATABASE,ALTER SYSTEM, etc.) are blocked. - Error isolation: internal error details (stack traces, SQL) are logged to stderr and never returned to the LLM.
Development
git clone https://github.com/madmarin/mcp-postgres
cd mcp-postgres
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,test]"
pre-commit install
Run the tests (requires Docker for testcontainers):
pytest
Lint and format:
ruff check src tests
ruff format src tests
mypy src
See CONTRIBUTING.md for details on how to add new tools or submit a PR.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_postgres_server-0.1.0.tar.gz.
File metadata
- Download URL: mcp_postgres_server-0.1.0.tar.gz
- Upload date:
- Size: 14.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f9bd24bf81d058a48cce8bdc034258cf488cdc5f3df13fed032a3d00bcbf44e0
|
|
| MD5 |
5369cadabe9003ec8979a7c5273cbf2e
|
|
| BLAKE2b-256 |
97bcc9917671e6266eae06c6c4f2bd7602ba3c6a4e93b639e219732ab64513ad
|
Provenance
The following attestation bundles were made for mcp_postgres_server-0.1.0.tar.gz:
Publisher:
release.yml on madmarin/mcp-postgres-server
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_postgres_server-0.1.0.tar.gz -
Subject digest:
f9bd24bf81d058a48cce8bdc034258cf488cdc5f3df13fed032a3d00bcbf44e0 - Sigstore transparency entry: 1182206394
- Sigstore integration time:
-
Permalink:
madmarin/mcp-postgres-server@27d831770f6ab662e29d4f855eeddaa4a9e692d3 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/madmarin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@27d831770f6ab662e29d4f855eeddaa4a9e692d3 -
Trigger Event:
push
-
Statement type:
File details
Details for the file mcp_postgres_server-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mcp_postgres_server-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da769b72d29631674f8098ed1a210aa3ee7888c00795749bb0f13f9b95c0dbb3
|
|
| MD5 |
bdc11649f00e0d2c98e50b22e5245254
|
|
| BLAKE2b-256 |
c2882e386865f76bccd5453edef5b0b709d39c52839e26049eaa9318cef5b995
|
Provenance
The following attestation bundles were made for mcp_postgres_server-0.1.0-py3-none-any.whl:
Publisher:
release.yml on madmarin/mcp-postgres-server
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_postgres_server-0.1.0-py3-none-any.whl -
Subject digest:
da769b72d29631674f8098ed1a210aa3ee7888c00795749bb0f13f9b95c0dbb3 - Sigstore transparency entry: 1182206396
- Sigstore integration time:
-
Permalink:
madmarin/mcp-postgres-server@27d831770f6ab662e29d4f855eeddaa4a9e692d3 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/madmarin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@27d831770f6ab662e29d4f855eeddaa4a9e692d3 -
Trigger Event:
push
-
Statement type: