Skip to main content
Glama
ArkTechNWA

PostgreSQL MCP Server

by ArkTechNWA
README.md
# postgres-mcp

[![CI](https://github.com/ArkTechNWA/postgres-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/ArkTechNWA/postgres-mcp/actions/workflows/ci.yml)

A Model Context Protocol (MCP) server for PostgreSQL integration. Give your AI assistant safe, controlled access to your databases.

**Status:** v0.7.0

**Author:** Claude + MOD

**License:** MIT

**Org:** [ArktechNWA](https://github.com/ArktechNWA)

---

## Why?

Your AI assistant can write SQL but can't see your schema, can't run queries to verify, can't explore your data model. It's guessing.

"Just give it database credentials" — bad idea. One missing index + large table = hung query = frozen assistant. One hallucinated DELETE = disaster. No guardrails, no recovery.

postgres-mcp is **an intelligent interface**, not a connection wrapper:

| Problem | postgres-mcp Solution |
|---------|----------------------|
| Queries can hang forever | NEVERHANG — adaptive timeouts, circuit breaker |
| No visibility into database health | Health monitoring with degraded state detection |
| Failures cascade | Circuit breaker opens, queries fail fast, auto-recovery |
| All-or-nothing access | Granular: read-only default, table blacklist, permission tiers |
| AI can't verify its SQL | Schema introspection + natural language queries |

**Prometheus tells you the database is on fire. NEVERHANG lets you walk through the fire without getting burned.**

---

## Philosophy

1. **Safety first** — Read-only by default, write explicitly enabled
2. **Query safety** — Statement timeouts, row limits, dangerous pattern blocking
3. **Schema awareness** — Introspection without data exposure
4. **NEVERHANG** — Circuit breaker, adaptive timeouts, health monitoring, graceful degradation
5. **Natural language** — Ask questions in plain English, get SQL + results

---

## Features

### Natural Language (v0.6)
- **pg_ask** — Ask questions in plain English, get SQL + results
- Powered by Claude Sonnet for accurate SQL generation
- Automatic schema context gathering
- Fallback mode works without API key (returns schema for caller to generate SQL)

### Perception (Read)
- Execute SELECT queries
- Schema introspection (tables, columns, indexes, constraints)
- **pg_schema** — Unified table view (columns + indexes + constraints in one call)
- **pg_sample** — Sample rows with blacklist filtering
- Explain query plans
- Database statistics
- Active connections and locks

### Action (Write)
- INSERT, UPDATE, DELETE (permission-gated)
- DDL operations (permission-gated)
- Transaction support

### Reliability (v0.5 NEVERHANG + v0.7 A.L.A.N.)
- Circuit breaker with automatic recovery
- Adaptive timeouts based on query complexity
- Health monitoring with degraded state handling
- Connection pool management
- **A.L.A.N. persistence:** Circuit state and query history survive restarts

---

## Permission Model

**CRITICAL:** Database access requires careful permission management.

### Permission Levels

| Level | Description | Default |
|-------|-------------|---------|
| `read` | SELECT queries, schema introspection | **ON** |
| `write` | INSERT, UPDATE, DELETE | OFF |
| `ddl` | CREATE, ALTER, DROP | OFF |
| `admin` | VACUUM, REINDEX, connection management | OFF |

### Table/Schema Filtering

```json
{
  "permissions": {
    "read": true,
    "write": false,
    "ddl": false,
    "admin": false,

    "whitelist_schemas": ["public", "app"],
    "blacklist_schemas": ["pg_catalog", "information_schema"],

    "whitelist_tables": [],
    "blacklist_tables": [
      "users.password_hash",
      "secrets.*",
      "*.credentials"
    ]
  }
}
```

**Rules:**
- Blacklist always wins
- Column-level filtering supported
- Pattern matching: `schema.table.column`

### Query Safety

```json
{
  "query_safety": {
    "statement_timeout": "30s",
    "max_rows": 1000,
    "block_patterns": [
      "DROP DATABASE",
      "TRUNCATE",
      "DELETE FROM .* WHERE 1=1",
      "UPDATE .* SET .* WHERE 1=1"
    ],
    "require_where_clause": true
  }
}
```

### Bypass Mode

```bash
postgres-mcp --bypass-permissions
```

Full database access. **DANGER ZONE.**

---

## Authentication

```json
{
  "connection": {
    "host": "localhost",
    "port": 5432,
    "database": "myapp",
    "user_env": "PGUSER",
    "password_env": "PGPASSWORD",
    "ssl": true
  }
}
```

Or connection string:
```json
{
  "connection": {
    "url_env": "DATABASE_URL"
  }
}
```

**Recommendation:** Use a read-only database user for maximum safety.

---

## Tools

### Queries

#### `pg_query`
Execute a SELECT query.

```typescript
pg_query({
  query: string,
  params?: any[],           // parameterized queries
  limit?: number,           // override max_rows
  timeout?: string          // override statement_timeout
})
```

Returns:
```json
{
  "query": "SELECT name, email FROM users WHERE active = $1",
  "params": [true],
  "rows": [
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"}
  ],
  "row_count": 2,
  "execution_time": "12ms",
  "summary": "2 active users found"
}
```

#### `pg_execute`
Execute INSERT/UPDATE/DELETE. Requires `write` permission.

```typescript
pg_execute({
  query: string,
  params?: any[],
  returning?: boolean       // add RETURNING *
})
```

Returns:
```json
{
  "query": "UPDATE users SET active = $1 WHERE id = $2",
  "params": [false, 123],
  "affected_rows": 1,
  "execution_time": "5ms"
}
```

### Natural Language (v0.6)

#### `pg_ask`
Ask a question in natural language — translates to SQL and executes.

```typescript
pg_ask({
  question: string,           // "How many users signed up this month?"
  tables?: string[],          // limit to specific tables
  schema?: string,            // default: "public"
  timeout_ms?: number         // override timeout
})
```

Returns:
```json
{
  "question": "How many users signed up this month?",
  "generated_sql": "SELECT COUNT(*) FROM users WHERE created >= DATE_TRUNC('month', CURRENT_DATE)",
  "rows": [{"count": "142"}],
  "row_count": 1,
  "execution_time": "3.2s"
}
```

**Fallback mode:** If `ANTHROPIC_API_KEY` is not set, returns schema context for the caller to generate SQL:
```json
{
  "mode": "fallback",
  "message": "pg_ask fallback mode activated. To enable direct NL→SQL via Sonnet, add ANTHROPIC_API_KEY to ~/.claude.json under mcpServers.postgres-mcp.env",
  "question": "How many users?",
  "schema_context": "CREATE TABLE users (id, email, created...)",
  "instructions": { "step_1": "Analyze schema", "step_2": "Generate SQL", "step_3": "Use pg_query" }
}
```

### Schema Introspection

#### `pg_tables`
List tables with metadata.

```typescript
pg_tables({
  schema?: string,          // default: "public"
  pattern?: string          // table name pattern
})
```

Returns:
```json
{
  "tables": [
    {
      "schema": "public",
      "name": "users",
      "type": "table",
      "row_estimate": 15420,
      "size": "2.3 MB",
      "description": "User accounts"
    }
  ]
}
```

#### `pg_columns`
Get column information for a table.

```typescript
pg_columns({
  table: string,
  schema?: string
})
```

Returns:
```json
{
  "table": "users",
  "columns": [
    {
      "name": "id",
      "type": "integer",
      "nullable": false,
      "default": "nextval('users_id_seq')",
      "primary_key": true
    },
    {
      "name": "email",
      "type": "varchar(255)",
      "nullable": false,
      "unique": true
    }
  ]
}
```

#### `pg_indexes`
Get index information.

```typescript
pg_indexes({
  table?: string,
  schema?: string
})
```

#### `pg_constraints`
Get constraint information (PK, FK, unique, check).

```typescript
pg_constraints({
  table?: string,
  schema?: string,
  type?: "PRIMARY KEY" | "FOREIGN KEY" | "UNIQUE" | "CHECK"
})
```

#### `pg_schema`
Get complete schema for a table (columns, indexes, constraints) in one call.

```typescript
pg_schema({
  table: string,
  schema?: string            // default: "public"
})
```

Returns:
```json
{
  "table": "users",
  "columns": [...],
  "indexes": [...],
  "constraints": [...]
}
```

#### `pg_sample`
Get sample rows from a table (respects column blacklist).

```typescript
pg_sample({
  table: string,
  schema?: string,            // default: "public"
  limit?: number,             // default: 5, max: 20
  order_by?: string           // default: primary key
})
```

Returns:
```json
{
  "table": "users",
  "sample_rows": [
    {"id": 1, "email": "alice@example.com", "created": "2025-01-01"},
    {"id": 2, "email": "bob@example.com", "created": "2025-01-02"}
  ],
  "columns_shown": 3,
  "columns_hidden": 1,
  "note": "password column hidden (blacklisted)"
}
```

### Query Analysis

#### `pg_explain`
Get query execution plan.

```typescript
pg_explain({
  query: string,
  params?: any[],
  analyze?: boolean,        // actually run (careful!)
  format?: "text" | "json"
})
```

Returns:
```json
{
  "query": "SELECT * FROM users WHERE email = $1",
  "plan": {
    "node_type": "Index Scan",
    "index_name": "users_email_idx",
    "estimated_rows": 1,
    "estimated_cost": 0.42
  },
  "summary": "Uses index scan on users_email_idx, estimated 1 row"
}
```

### Statistics

#### `pg_stats`
Get database/table statistics.

```typescript
pg_stats({
  table?: string,           // specific table (omit for database)
  include_index_usage?: boolean
})
```

#### `pg_connections`
Get active connections.

```typescript
pg_connections({
  include_queries?: boolean
})
```

#### `pg_locks`
Get current locks.

```typescript
pg_locks({
  blocked_only?: boolean
})
```

### Analysis

#### `pg_analyze_query`
AI-powered query analysis.

```typescript
pg_analyze_query({
  query: string,
  use_ai?: boolean
})
```

Returns:
```json
{
  "query": "SELECT * FROM orders WHERE user_id = 123",
  "plan_summary": "Sequential scan on orders (15M rows)",
  "synthesis": {
    "analysis": "This query performs a full table scan. The user_id column is not indexed.",
    "suggested_index": "CREATE INDEX orders_user_id_idx ON orders(user_id);",
    "estimated_improvement": "~10,000x faster",
    "confidence": "high"
  }
}
```

#### `pg_suggest_schema`
Get schema improvement suggestions.

```typescript
pg_suggest_schema({
  table: string,
  use_ai?: boolean
})
```

---

## NEVERHANG v2.0 Architecture

Database queries can hang indefinitely. A missing index + large table = disaster. NEVERHANG is a multi-layered reliability system that ensures postgres-mcp never blocks your AI assistant.

### Circuit Breaker
- **Automatic trip:** 3 failures in 60s → circuit opens
- **Cooldown:** 5 minute recovery period
- **Health states:** `healthy` → `degraded` → `unhealthy`
- **Graceful degradation:** Returns cached/safe responses when circuit is open

### Adaptive Timeouts
- **Query complexity analysis:** Simple queries get shorter timeouts
- **Pattern recognition:** Known-slow patterns (JOINs, subqueries) get longer timeouts
- **Learning:** Adjusts based on historical query performance
- **Override:** Per-query timeout always available

### Health Monitor
- **Continuous ping:** Background health checks
- **State tracking:** Monitors connection pool health
- **Recovery detection:** Automatic circuit close when health returns
- **Metrics:** Success rate, average latency, failure patterns

### Connection Management
- **Pool limits:** Configurable min/max connections
- **Idle timeout:** Releases unused connections (default: 60s)
- **Connection timeout:** Fast fail on connection issues (default: 10s)

### Row Limits
- **Default max:** 1000 rows
- **Auto-LIMIT injection:** Adds LIMIT to unbounded SELECTs
- **Prevents:** Accidental `SELECT *` disasters

### A.L.A.N. Persistence (v0.7)
**As Long As Necessary** — persistent memory for NEVERHANG:
- **Circuit state survives restarts:** No cold-start amnesia
- **Query history tracking:** 7 days of execution metrics
- **P95 latency by complexity:** Adaptive timeout learning
- **Health check logs:** 24 hours for trend analysis
- **Location:** `~/.cache/postgres-mcp/neverhang.db` (XDG compliant)
- **Auto-cleanup:** Prunes old data on startup

```json
{
  "neverhang": {
    "statement_timeout": "30s",
    "connect_timeout": "10s",
    "max_rows": 1000,
    "circuit_breaker": {
      "failures": 3,
      "window": 60000,
      "cooldown": 300000
    }
  }
}
```

---

## AI Integration (v0.6)

`pg_ask` uses Claude Sonnet to translate natural language to SQL.

**Configuration:** Add `ANTHROPIC_API_KEY` to your MCP server environment:

```json
{
  "mcpServers": {
    "postgres-mcp": {
      "command": "node",
      "args": ["/path/to/postgres-mcp/dist/index.js"],
      "env": {
        "PGHOST": "localhost",
        "PGDATABASE": "myapp",
        "ANTHROPIC_API_KEY": "sk-ant-..."
      }
    }
  }
}
```

**Fallback mode:** If no API key is set, `pg_ask` returns schema context with instructions for the caller to generate SQL. This allows the tool to provide value even without a separate API key.

---

## Configuration

`~/.config/postgres-mcp/config.json`:

```json
{
  "connection": {
    "host": "localhost",
    "port": 5432,
    "database": "myapp",
    "user_env": "PGUSER",
    "password_env": "PGPASSWORD"
  },
  "permissions": {
    "read": true,
    "write": false,
    "ddl": false,
    "admin": false,
    "blacklist_tables": ["*.password*", "*.secret*"]
  },
  "query_safety": {
    "statement_timeout": "30s",
    "max_rows": 1000,
    "require_where_clause": true
  },
  "fallback": {
    "enabled": false
  }
}
```

### Claude Code Integration

```json
{
  "mcpServers": {
    "postgres": {
      "command": "postgres-mcp",
      "env": {
        "PGUSER": "readonly_user",
        "PGPASSWORD": "secret"
      }
    }
  }
}
```

---

## Installation

```bash
npm install -g @arktechnwa/postgres-mcp
```

---

## Requirements

- Node.js 18+
- PostgreSQL 12+
- Optional: Anthropic API key for fallback AI

---

## Security Considerations

1. **Use read-only user** — Create a DB user with SELECT-only grants
2. **Blacklist sensitive tables** — Passwords, secrets, PII
3. **Statement timeout** — Prevent runaway queries
4. **Row limits** — Prevent accidental data dumps
5. **No credential exposure** — Connection strings never logged

---

## Credits

Created by Claude (claude@arktechnwa.com) in collaboration with Meldrey.
Part of the [ArktechNWA MCP Toolshed](https://github.com/ArktechNWA).