MCP Server for postgresql Databases including SSL and token based authetification
3.4K
Connects Claude to PostgreSQL via the Model Context Protocol (MCP).
| Mode | Transport | When to use |
|---|---|---|
| Local (Node.js) | stdio | Development, no Docker |
| Docker / Remote | HTTP or HTTPS | Different host on the network |
| Kubernetes | HTTP or HTTPS | Production, Helm chart |
| Variable | Default | Description |
|---|---|---|
TRANSPORT | stdio | stdio or http |
PORT | 3000 | HTTP(S) port |
AUTH_TOKEN | – | Admin token for /mcp and /admin/tokens (empty = auth disabled) |
TOKENS_FILE | ./tokens.json | Path to the JSON file that stores tokens and their connection configs |
TLS_ENABLED | false | true → HTTPS, false → HTTP |
TLS_CERT_FILE | /certs/tls.crt | Server certificate (PEM) |
TLS_KEY_FILE | /certs/tls.key | Server 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_HOST | localhost | Default PostgreSQL host (used when a token has no custom connection) |
PG_PORT | 5432 | Default PostgreSQL port |
PG_DATABASE | postgres | Default database name |
PG_USER | postgres | Default username |
PG_PASSWORD | – | Default password |
PG_SSL | false | Default 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 |
The image is available on Docker Hub:
docker pull tommi2day/pg-mcp-server:latest
| Tag | Description |
|---|---|
latest | Latest build from main |
1.2.3 | Specific version |
1.2 | Latest patch of 1.2 |
sha-abc1234 | Specific commit |
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
Use the Hub image instead of building locally:
services:
pg-mcp-server:
image: tommi2day/pg-mcp-server:latest
# build: . ← remove or comment out
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"
}
}
}
}
run.shscripts/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).
AUTH_TOKEN on first run and saves it to ./auth_token.env from the project root if present# 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
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:
| Variable | Default | Description |
|---|---|---|
AUTH_TOKEN | (empty) | Admin bearer token; leave empty to disable auth |
MCP_PORT | 3000 | Host port for the MCP server |
PG_HOST | postgres-test | PostgreSQL host (use host.docker.internal for a local DB outside Docker) |
PG_DATABASE | testdb | Database name |
PG_USER | postgres | Database user |
PG_PASSWORD | postgres | Database password |
PG_SSL | false | false / true / verify |
TLS_ENABLED | false | true to enable HTTPS |
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 automaticallyOnce running, open http://localhost:3000/admin to manage tokens via the web UI.
.mcp.json{
"mcpServers": {
"postgresql": {
"type": "http",
"url": "http://<HOST>:3000/mcp",
"headers": {
"Authorization": "Bearer <AUTH_TOKEN>"
}
}
}
}
Replace http:// with https:// for HTTPS.
kubectl configuredhelm v3 installedhelm 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)
# 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
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
helm upgrade pg-mcp ./helm/pg-mcp-server -n mcp -f my-values.yaml
helm uninstall pg-mcp -n mcp
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.
Open http://<HOST>:3000/admin in a browser. The web interface lets you manage tokens without using the command line or curl.
AUTH_TOKEN value. Leave the token field empty if auth is disabled.active = false.The session is stored in sessionStorage (cleared when the browser tab is closed).
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.
token.shtoken.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
test_token.shConnects 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).
# 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"
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.
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.
Dependabot is configured to check for updates weekly for:
production and dev)The Dependabot Automerge workflow automatically enables auto-merge for Dependabot PRs.
# 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.
| Tool | Description |
|---|---|
test_connection | Check connection and TLS status |
list_schemas | List all schemas |
list_tables | List tables in a schema |
describe_table | Show columns, types and constraints |
query | Execute SELECT (max 200 rows) |
execute | Execute INSERT / UPDATE / DELETE / DDL |
| Path | Auth | Description |
|---|---|---|
POST /mcp | Admin or file token | MCP Streamable-HTTP (uses token's connection if set) |
GET /health | none | Health check ({"status":"ok","tls":<bool>}) |
GET /admin | none | Web-based token administration UI |
GET /admin/tokens | Admin token only | List tokens with connection info (no hashes) |
POST /admin/tokens | Admin token only | Create token; optional connection object |
PATCH /admin/tokens/:id | Admin token only | Update name, active, and/or connection |
DELETE /admin/tokens/:id | Admin token only | Deactivate token |
A full OpenAPI 3.1 specification is available in openapi.json.
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.
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).
Go to Actions → Release → Run workflow, enter a version number (e.g. 1.2.3), and click Run workflow.
The workflow will:
package.json, openapi.json and helm/pg-mcp-server/Chart.yaml to the entered version, commit and push to maintommi2day/pg-mcp-server:1.2.3, :1.2, :1, :latest, :sha-<short>)| Tag | Example |
|---|---|
| Full version | 1.2.3 |
| Major.minor | 1.2 |
| Major | 1 |
| Latest | latest |
| Commit SHA | sha-abc1234 |
Content type
Image
Digest
sha256:b404fd738…
Size
64 MB
Last updated
2 months ago
docker pull tommi2day/pg-mcp-server