Sign inSign up

tommi2day/pg-mcp-server

By tommi2day

•Updated 2 months ago

MCP Server for postgresql Databases including SSL and token based authetification

Image
Machine learning & AI
Data science
Databases & storage
0

3.4K

tommi2day/pg-mcp-server repository overview

⁠PostgreSQL MCP Server

Connects Claude to PostgreSQL via the Model Context Protocol (MCP).

CI codecov GitHub release (latest SemVer) Docker Pulls

⁠Overview

ModeTransportWhen to use
Local (Node.js)stdioDevelopment, no Docker
Docker / RemoteHTTP or HTTPSDifferent host on the network
KubernetesHTTP or HTTPSProduction, Helm chart

⁠Environment Variables

VariableDefaultDescription
TRANSPORTstdiostdio or http
PORT3000HTTP(S) port
AUTH_TOKEN–Admin token for /mcp and /admin/tokens (empty = auth disabled)
TOKENS_FILE./tokens.jsonPath to the JSON file that stores tokens and their connection configs
TLS_ENABLEDfalsetrue → HTTPS, false → HTTP
TLS_CERT_FILE/certs/tls.crtServer certificate (PEM)
TLS_KEY_FILE/certs/tls.keyServer key (PEM)
TLS_CA_FILE–Client CA for mTLS (optional)
TLS_SAN–Additional SANs for self-signed cert, e.g. DNS:myhost,IP:1.2.3.4
PG_HOSTlocalhostDefault PostgreSQL host (used when a token has no custom connection)
PG_PORT5432Default PostgreSQL port
PG_DATABASEpostgresDefault database name
PG_USERpostgresDefault username
PG_PASSWORD–Default password
PG_SSLfalseDefault SSL mode: false / true / verify
PG_SSL_CA_FILE–CA for PostgreSQL certificate (when PG_SSL=verify)
PG_SSL_CERT_FILE–Client certificate for PostgreSQL mTLS
PG_SSL_KEY_FILE–Client key for PostgreSQL mTLS

⁠Docker Hub

The image is available on Docker Hub:

docker pull tommi2day/pg-mcp-server:latest
TagDescription
latestLatest build from main
1.2.3Specific version
1.2Latest patch of 1.2
sha-abc1234Specific commit
⁠Quick start from Hub
docker run -d --name pg-mcp-server \
  -p 3000:3000 \
  --add-host=host.docker.internal:host-gateway \
  -e TRANSPORT=http \
  -e AUTH_TOKEN=$(openssl rand -hex 32) \
  -e TOKENS_FILE=/data/tokens.json \
  -e PG_HOST=host.docker.internal \
  -e PG_DATABASE=mydb \
  -e PG_USER=user \
  -e PG_PASSWORD=password \
  -v pg-mcp-data:/data \
  tommi2day/pg-mcp-server:latest
⁠In docker-compose.yml

Use the Hub image instead of building locally:

services:
  pg-mcp-server:
    image: tommi2day/pg-mcp-server:latest
    # build: .   ← remove or comment out

⁠1 · Local (stdio)

npm install
node index.js

claude_desktop_config.json:

{
  "mcpServers": {
    "postgresql": {
      "command": "node",
      "args": ["/path/to/index.js"],
      "env": {
        "PG_HOST": "localhost",
        "PG_DATABASE": "mydb",
        "PG_USER": "user",
        "PG_PASSWORD": "password"
      }
    }
  }
}

⁠2 · Docker

⁠Quick start with run.sh

scripts/run.sh builds and starts the container in one step:

# Optionally configure the PostgreSQL connection via .env in the project root
cp .env.example .env
# edit .env: set PG_HOST, PG_DATABASE, PG_USER, PG_PASSWORD, ...

./scripts/run.sh              # start as "pg-mcp-server"
./scripts/run.sh my-name      # start with a custom container name

run.sh reads PGHOST / PGPORT / PGDATABASE / PGUSER / PGPASSWORD / PG_SSL from .env and auto-generates AUTH_TOKEN on first run (saved to ./auth_token).

  • Stops and removes any existing container with the same name
  • Auto-generates AUTH_TOKEN on first run and saves it to ./auth_token
  • Reads .env from the project root if present
⁠Quick start (manual)
# Build image
docker build -t pg-mcp-server .

# Run against a local PostgreSQL
docker run -d --name pg-mcp-server \
  -p 3000:3000 \
  --add-host=host.docker.internal:host-gateway \
  -e TRANSPORT=http \
  -e AUTH_TOKEN=$(openssl rand -hex 32) \
  -e TOKENS_FILE=/data/tokens.json \
  -e PG_HOST=host.docker.internal \
  -e PG_DATABASE=mydb \
  -e PG_USER=user \
  -e PG_PASSWORD=password \
  -v pg-mcp-data:/data \
  pg-mcp-server
⁠With docker-compose (including test database)

Copy the example env file, edit it, then start:

cp .env.example .env
# edit .env: set AUTH_TOKEN, PG_PASSWORD, etc.

docker compose up -d
docker compose logs -f pg-mcp-server

docker compose automatically reads .env from the project root. The docker-compose.yml includes a postgres-test container (port 5433) that must be healthy before pg-mcp-server starts (depends_on: condition: service_healthy).

Key variables in .env:

VariableDefaultDescription
AUTH_TOKEN(empty)Admin bearer token; leave empty to disable auth
MCP_PORT3000Host port for the MCP server
PG_HOSTpostgres-testPostgreSQL host (use host.docker.internal for a local DB outside Docker)
PG_DATABASEtestdbDatabase name
PG_USERpostgresDatabase user
PG_PASSWORDpostgresDatabase password
PG_SSLfalsefalse / true / verify
TLS_ENABLEDfalsetrue to enable HTTPS
⁠Enable TLS (optional)
environment:
  TLS_ENABLED: "true"
  TLS_SAN: "DNS:my-host.local,IP:192.168.1.10"
volumes:
  - ./certs:/certs   # mount real certs; leave empty → self-signed is generated

On startup:

  • /certs contains a certificate → it is used (permissions are adjusted automatically)
  • /certs is empty → a self-signed certificate is generated automatically

Once running, open http://localhost:3000/admin to manage tokens via the web UI.

⁠Connect Claude Desktop / .mcp.json
{
  "mcpServers": {
    "postgresql": {
      "type": "http",
      "url": "http://<HOST>:3000/mcp",
      "headers": {
        "Authorization": "Bearer <AUTH_TOKEN>"
      }
    }
  }
}

Replace http:// with https:// for HTTPS.


⁠3 · Kubernetes with Helm

⁠Prerequisites
  • kubectl configured
  • helm v3 installed
  • Image accessible in a registry
⁠Quick start (HTTP, no TLS)
helm install pg-mcp ./helm/pg-mcp-server \
  --namespace mcp --create-namespace \
  --set image.repository=tommi2day/pg-mcp-server \
  --set postgresql.host=my-db-host \
  --set postgresql.database=mydb \
  --set postgresql.user=user \
  --set postgresql.password=secret \
  --set auth.token=$(openssl rand -hex 32)
⁠With HTTPS
# Create TLS secret
kubectl create secret tls pg-mcp-tls \
  --cert=certs/tls.crt --key=certs/tls.key -n mcp

# Create auth secret
kubectl create secret generic my-auth-secret \
  --from-literal=token=$(openssl rand -hex 32) -n mcp

# PostgreSQL CA (only for PG_SSL=verify)
kubectl create secret generic pg-ca-cert \
  --from-file=ca.crt=certs/pg-ca.crt -n mcp

helm install pg-mcp ./helm/pg-mcp-server \
  --namespace mcp --create-namespace \
  --set server.tlsEnabled=true \
  --set tls.existingSecret=pg-mcp-tls \
  --set auth.existingSecret=my-auth-secret \
  --set postgresql.ssl=verify \
  --set tls.pgCaSecret=pg-ca-cert \
  --set image.repository=tommi2day/pg-mcp-server \
  --set postgresql.host=my-db-host \
  --set postgresql.database=mydb \
  --set postgresql.user=user \
  --set postgresql.existingSecret=pg-credentials
⁠Production values.yaml
image:
  repository: tommi2day/pg-mcp-server
  tag: "latest"

replicaCount: 2

persistence:
  enabled: true
  size: 50Mi
  storageClass: "standard"

auth:
  existingSecret: "my-auth-secret"

postgresql:
  host: "rds.example.com"
  database: "prod_db"
  user: "prod_user"
  ssl: "verify"
  existingSecret: "pg-credentials"

server:
  tlsEnabled: true

tls:
  existingSecret: "pg-mcp-tls"
  pgCaSecret: "pg-ca-cert"

service:
  type: LoadBalancer

ingress:
  enabled: true
  className: nginx
  annotations:
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: pg-mcp.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: pg-mcp-ingress-tls
      hosts:
        - pg-mcp.example.com

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
⁠Upgrade / Uninstall
helm upgrade pg-mcp ./helm/pg-mcp-server -n mcp -f my-values.yaml
helm uninstall pg-mcp -n mcp

⁠Authentication

AUTH_TOKEN (env var) is the admin token — it grants access to /mcp and the token management API. Additional file tokens can be created via the admin UI or API; they only have access to /mcp. Token values are stored as SHA-256 hashes in a local JSON file (TOKENS_FILE); plaintext is shown only once at creation and never stored.

Each file token can optionally have its own PostgreSQL connection. When a token has no custom connection, it uses the server's default connection (PG_HOST / PG_DATABASE / … env vars).

No AUTH_TOKEN set → auth is completely disabled (local/dev only). The admin UI still works but does not require a token.

⁠Admin UI

Open http://<HOST>:3000/admin in a browser. The web interface lets you manage tokens without using the command line or curl.

  • Login: enter the server URL and the AUTH_TOKEN value. Leave the token field empty if auth is disabled.
  • Token list: see all tokens with status (active/inactive), connection info, and last-used timestamp.
  • Create token: enter a name and optionally configure a custom PostgreSQL connection. The generated token value is shown once — copy it before closing.
  • Edit token: rename a token, toggle its active state, or update its database connection.
  • Deactivate token: revokes access immediately; the record is kept in the store with active = false.

The session is stored in sessionStorage (cleared when the browser tab is closed).

⁠Token store

Tokens are persisted in a JSON file (default ./tokens.json, configurable via TOKENS_FILE).
Mount a volume at the file's directory so tokens survive container restarts — see the Docker and Helm sections above.

⁠Manage tokens with token.sh

token.sh reads AUTH_TOKEN and MCP_URL from environment variables or from a scripts/.env file:

# Option A – environment variables
export AUTH_TOKEN=<admin-token>
export MCP_URL=http://localhost:3000   # optional, default

# Option B – scripts/.env file
cat > scripts/.env <<EOF
AUTH_TOKEN=<admin-token>
MCP_URL=http://localhost:3000
EOF

When using run.sh, the generated token is stored in ./auth_token:

export AUTH_TOKEN=$(cat auth_token)
./scripts/token.sh list                     # list all tokens (with connection info)
./scripts/token.sh add "claude-desktop"     # create new token (plaintext shown once)
./scripts/token.sh delete <id>              # deactivate token
./scripts/token.sh disable <id>             # temporarily block
./scripts/token.sh enable  <id>             # re-enable
./scripts/token.sh rename  <id> <new-name>  # rename

# Per-token database connection
PG_HOST=db.example.com PG_DATABASE=mydb PG_USER=u PG_PASSWORD=p \
  ./scripts/token.sh add "mydb-client"      # create token with custom connection

./scripts/token.sh setconn <id> '{"host":"db.example.com","port":5432,"database":"mydb","user":"u","password":"p"}'
./scripts/token.sh clearconn <id>           # reset to default admin connection
⁠Validate a token with test_token.sh

Connects to the server using the given token and lists tables — useful to confirm a newly created token works:

./scripts/test_token.sh <token>              # schema: public (default)
./scripts/test_token.sh <token> myschema     # specific schema

Reads MCP_URL from environment or scripts/.env. Exits with a clear error message on failure (invalid token, server unreachable, MCP tool error, etc.).

The script sends an X-Real-IP header so the server logs the real client IP. The value is taken from X_REAL_IP env var if set, otherwise auto-detected from the first local interface (hostname -I).

⁠Manage tokens with curl
# List tokens (includes connection info; token_hash is never returned)
curl http://localhost:3000/admin/tokens \
  -H "Authorization: Bearer $AUTH_TOKEN"

# Create token (default connection)
curl -X POST http://localhost:3000/admin/tokens \
  -H "Authorization: Bearer $AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "claude-desktop"}'

# Create token with a custom DB connection
curl -X POST http://localhost:3000/admin/tokens \
  -H "Authorization: Bearer $AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "mydb-client",
    "connection": {
      "host": "db.example.com",
      "port": 5432,
      "database": "mydb",
      "user": "myuser",
      "password": "secret",
      "ssl": "false"
    }
  }'

# Set or update the connection on an existing token
curl -X PATCH http://localhost:3000/admin/tokens/<id> \
  -H "Authorization: Bearer $AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"connection": {"host": "db.example.com", "database": "mydb", "user": "u", "password": "p"}}'

# Clear per-token connection (fall back to default admin connection)
curl -X PATCH http://localhost:3000/admin/tokens/<id> \
  -H "Authorization: Bearer $AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"connection": null}'

# Rename / re-enable token
curl -X PATCH http://localhost:3000/admin/tokens/<id> \
  -H "Authorization: Bearer $AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "new-name", "active": true}'

# Deactivate token
curl -X DELETE http://localhost:3000/admin/tokens/<id> \
  -H "Authorization: Bearer $AUTH_TOKEN"
⁠Connection logging

Every authenticated request is logged to stderr with a timestamp, the token name, action, client IP, and tool parameters:

[2026-03-28T19:32:51.654Z] [MCP]   token="claude-desktop" action="list_tables" ip="192.168.1.10" params={"schema":"public"}
[2026-03-28T19:32:51.859Z] [ADMIN] token="admin"          action="POST /admin/tokens" ip="192.168.1.10"
  • [MCP] — MCP tool calls; token name is "admin" for the env token, "anonymous" when auth is disabled, or the file token's name; params is omitted for tools with no arguments
  • [ADMIN] — admin API requests; always token="admin"

The client IP is resolved in order: x-real-ip header → first entry of x-forwarded-for → TCP socket address. When running Docker without a reverse proxy, the socket address is the Docker bridge IP — deploy behind nginx or Traefik to log the real client IP.

⁠Token file format

The token store is a plain JSON file. The server reads and writes it automatically — do not edit it while the server is running.

{
  "tokens": [
    {
      "id": 1,
      "name": "claude-desktop",
      "token_hash": "<sha256-hex>",
      "created_at": "2026-04-03T10:00:00.000Z",
      "last_used_at": "2026-04-03T12:34:56.789Z",
      "active": true,
      "connection": null
    },
    {
      "id": 2,
      "name": "mydb-client",
      "token_hash": "<sha256-hex>",
      "created_at": "2026-04-03T10:05:00.000Z",
      "last_used_at": null,
      "active": true,
      "connection": {
        "host": "db.example.com",
        "port": 5432,
        "database": "mydb",
        "user": "myuser",
        "password": "secret",
        "ssl": "false"
      }
    }
  ],
  "next_id": 3
}

connection: null means the token uses the server's default PostgreSQL connection. The token_hash field is a SHA-256 hex digest — the plaintext token is never stored.

⁠Automated Updates

Dependabot is configured to check for updates weekly for:

  • npm dependencies (grouped into production and dev)
  • Docker base images
  • GitHub Actions

The Dependabot Automerge⁠ workflow automatically enables auto-merge for Dependabot PRs.


⁠Development

# Install dependencies
npm install

# Run tests
./scripts/test.sh                        # all tests
./scripts/test.sh tests/lib.test.js      # single file

# Coverage report
./scripts/coverage.sh                    # report written to ./coverage/
./scripts/coverage.sh --open             # open HTML report in browser

# Linting
./scripts/lint.sh                        # check all files
./scripts/lint.sh --fix                  # auto-fix issues

All scripts require only Docker — no local Node.js needed.


⁠Available MCP Tools

ToolDescription
test_connectionCheck connection and TLS status
list_schemasList all schemas
list_tablesList tables in a schema
describe_tableShow columns, types and constraints
queryExecute SELECT (max 200 rows)
executeExecute INSERT / UPDATE / DELETE / DDL

⁠Endpoints

PathAuthDescription
POST /mcpAdmin or file tokenMCP Streamable-HTTP (uses token's connection if set)
GET /healthnoneHealth check ({"status":"ok","tls":<bool>})
GET /adminnoneWeb-based token administration UI
GET /admin/tokensAdmin token onlyList tokens with connection info (no hashes)
POST /admin/tokensAdmin token onlyCreate token; optional connection object
PATCH /admin/tokens/:idAdmin token onlyUpdate name, active, and/or connection
DELETE /admin/tokens/:idAdmin token onlyDeactivate token

A full OpenAPI 3.1 specification is available in openapi.json⁠.


⁠Release

The release workflow (.github/workflows/release.yml) runs lint, tests, builds and pushes the Docker image, and creates a GitHub Release with auto-generated notes.

⁠Option 1 — Push a git tag

Use npm version to bump all version files together, then push the tag:

npm version 1.2.3   # bumps package.json, openapi.json and Chart.yaml, commits, creates git tag
git push origin main 1.2.3

The version lifecycle script keeps openapi.json and helm/pg-mcp-server/Chart.yaml in sync automatically. The tag must match [0-9]+.[0-9]+.[0-9]+ (e.g. 1.2.3, no v prefix).

⁠Option 2 — Manual dispatch (no local git required)

Go to Actions → Release → Run workflow, enter a version number (e.g. 1.2.3), and click Run workflow.

The workflow will:

  1. Bump package.json, openapi.json and helm/pg-mcp-server/Chart.yaml to the entered version, commit and push to main
  2. Run lint and tests
  3. Build and push the Docker image (tommi2day/pg-mcp-server:1.2.3, :1.2, :1, :latest, :sha-<short>)
  4. Create and push the git tag automatically
  5. Publish a GitHub Release with auto-generated notes
⁠Docker image tags per release
TagExample
Full version1.2.3
Major.minor1.2
Major1
Latestlatest
Commit SHAsha-abc1234

Tag summary

Content type

Image

Digest

sha256:b404fd738…

Size

64 MB

Last updated

2 months ago

docker pull tommi2day/pg-mcp-server