A Model Context Protocol (MCP) server that enables LLMs to interact with PostgreSQL databases through a layered tool architecture for progressive schema discovery and query execution.
Layer 1 - Schema Discovery:
list_schemas- Enumerate all database schemas with metadatalist_tables- List tables and views with row counts and sizesdescribe_table- Get detailed table structure (columns, indexes, constraints)get_sample_rows- Retrieve sample data to understand patterns
Layer 2 - Relationship Discovery:
get_foreign_keys- Get incoming and outgoing foreign key relationshipsfind_join_path- Discover join paths between tables via FK relationships
Layer 3 - Query Execution:
execute_query- Execute read-only SQL queries with parameterized valuesexplain_query- Get query execution plans for performance analysis
- Read-only by design: Only SELECT queries are allowed
- Parameterized queries: All user values use $1, $2 placeholders
- Blocked operations: INSERT, UPDATE, DELETE, DROP, and other write operations are blocked
- Query timeout: Configurable statement timeout prevents runaway queries
# Clone the repository
git clone https://github.com/yourusername/pg-mcp-server.git
cd pg-mcp-server
# Install dependencies with UV
uv sync
# Install with dev dependencies
uv sync --all-extrasCopy .env.example to .env and configure:
cp .env.example .envDatabase Settings (PG_* prefix):
| Variable | Description | Default |
|---|---|---|
PG_HOST |
PostgreSQL host | localhost |
PG_PORT |
PostgreSQL port | 5432 |
PG_DATABASE |
Database name | (required) |
PG_USER |
Database user | (required) |
PG_PASSWORD |
Database password | (required) |
PG_POOL_SIZE |
Connection pool size | 5 |
PG_STATEMENT_TIMEOUT |
Query timeout (ms) | 30000 |
PG_DEFAULT_SCHEMA |
Default schema | public |
Server Settings (MCP_* prefix):
| Variable | Description | Default |
|---|---|---|
MCP_TRANSPORT |
Transport type (stdio or http) |
stdio |
MCP_HOST |
HTTP server host | 0.0.0.0 |
MCP_PORT |
HTTP server port | 8080 |
MCP_LOG_LEVEL |
Logging level | INFO |
MCP_LOG_FORMAT |
Log format (json or text) |
json |
# Start server (auto-loads .env from current directory if present)
uv run pg-mcp-server
# Start server with custom .env file
uv run pg-mcp-server --env-file /path/to/.env
# Test database connection
uv run pg-mcp-server test
# Test with custom .env file
uv run pg-mcp-server --env-file /path/to/.env testEnvironment file resolution:
- If
--env-fileis specified, use that file - Otherwise, auto-load
.envfrom the current working directory (if it exists) - Fall back to environment variables if no
.envfile is found
uv run pg-mcp-serverMCP_TRANSPORT=http MCP_PORT=8080 uv run pg-mcp-serverAdd to your claude_desktop_config.json:
{
"mcpServers": {
"postgres": {
"command": "uv",
"args": ["run", "--directory", "/path/to/pg-mcp-server", "pg-mcp-server"],
"env": {
"PG_HOST": "localhost",
"PG_DATABASE": "myapp",
"PG_USER": "readonly_user",
"PG_PASSWORD": "your_password"
}
}
}
}list_schemas()
-> {"schemas": [{"name": "public", "table_count": 15}], "total_count": 1}
list_tables(schema_name="public")
-> {"tables": [{"name": "users", "estimated_row_count": 10000, ...}], "total_count": 15}
describe_table(table_name="users")
-> {"columns": [{"name": "id", "data_type": "int4", "is_primary_key": true}, ...]}
get_sample_rows(table_name="users", limit=5)
-> {"rows": [{"id": 1, "email": "user@example.com"}, ...], "row_count": 5}
get_foreign_keys(table_name="orders")
-> {"outgoing": [{"to_table": "users", ...}], "incoming": [{"from_table": "order_items", ...}]}
find_join_path(from_table="order_items", to_table="users")
-> {"paths": [{"sql_example": "FROM order_items JOIN orders ON ... JOIN users ON ...", ...}]}
execute_query(sql="SELECT * FROM users WHERE status = $1", params=["active"], limit=100)
-> {"rows": [...], "row_count": 42, "execution_time_ms": 12.5}
explain_query(sql="SELECT * FROM orders WHERE status = 'pending'")
-> {"plan": "Seq Scan on orders (cost=0.00..100.00 rows=1000 width=48)\n Filter: ..."}
uv run pytestuv run pytest --cov=pg_mcp_serveruv run mypy src/pg_mcp_serveruv run ruff check src testsuv run ruff format src testssrc/pg_mcp_server/
├── __init__.py # Package version
├── __main__.py # Entry point
├── server.py # FastMCP server with lifespan
├── config.py # Pydantic settings
├── errors.py # Error handling
├── database/
│ ├── engine.py # SQLAlchemy async engine
│ ├── schema.py # Schema discovery queries
│ ├── relationships.py # FK discovery queries
│ └── queries.py # Query execution
├── tools/
│ ├── schema_tools.py # Layer 1 tools
│ ├── relationship_tools.py # Layer 2 tools
│ └── query_tools.py # Layer 3 tools
└── models/
├── schema.py # Schema Pydantic models
├── relationships.py # Relationship Pydantic models
└── results.py # Query result models
- Python 3.11+
- PostgreSQL 14+ (tested with 14, 15, 16)
MIT License - see LICENSE file for details.