Zlash65
/
postgresql-ssh-mcp
PostgreSQL MCP server with SSH tunneling for Claude Desktop and ChatGPT
PostgreSQL SSH MCP Server
A secure PostgreSQL MCP server with built-in SSH tunneling. Connect to databases through bastion hosts automatically — no manual ssh -L required.
Features
- Dual Transport — STDIO for Claude Desktop, Streamable HTTP for ChatGPT
- SSH Tunneling — Built-in tunnel with auto-reconnect and TOFU (trust on first use)
- Read-Only by Default — Safe for production; enable writes explicitly
- OAuth Support — Auth0 integration for secure ChatGPT connections
- Connection Pooling — Efficient resource management with configurable limits
Architecture
Quick Start
Claude Desktop (STDIO)
Add to your Claude Desktop config:
| Platform | Config Location |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%/Claude/claude_desktop_config.json |
{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "@zlash65/postgresql-ssh-mcp"],
"env": {
"DATABASE_URI": "postgresql://user:password@localhost:5432/mydb"
}
}
}
}
ChatGPT (Streamable HTTP)
DATABASE_URI="postgresql://user:pass@localhost:5432/mydb" npx @zlash65/postgresql-ssh-mcp-http
Then configure ChatGPT to connect to…
Table of Contents
Why SSH Tunneling Matters
Production databases live in private networks (VPCs, private subnets) isolated from the public internet. You can't simply provide a DATABASE_URL to an AI and expect it to connect.
Companies use bastion hosts as secure gateways to private infrastructure—servers that authenticate via SSH and act as the only entry point to internal resources.
Without SSH Tunneling:
AI → Database URL → Connection Refused (private network)
With SSH Tunneling (this server):
AI → MCP Server → SSH Tunnel → Bastion → Database
Traditional approach (manual):
# Terminal 1: SSH tunnel
ssh -L 5432:db.internal:5432 user@bastion.company.com
# Terminal 2: MCP server
DATABASE_URI=postgresql://localhost:5432/db npx @zlash65/postgresql-ssh-mcp
This server (automatic):
{
"env": {
"DATABASE_URI": "postgresql://db.internal:5432/db",
"SSH_ENABLED": "true",
"SSH_HOST": "bastion.company.com",
"SSH_PRIVATE_KEY_PATH": "~/.ssh/id_rsa"
}
}
The server handles tunnel establishment, database traffic forwarding, auto-reconnection on failure, and clean shutdown automatically.
Without built-in SSH tunneling, you're stuck with bad options: VPN for everyone (overhead), exposed databases (security risk), or manual SSH tunneling (requires terminal skills). Product managers, operations teams, and analysts who need database insights shouldn't have to learn SSH commands or manage private keys. Configure it once, and it just works.
Key Features:
- 🔐 Automatic SSH tunneling with trust-on-first-use and auto-reconnection
- 🛡️ Read-only by default with smart SQL validation (60+ blocked patterns)
- 🔄 Dual transport — STDIO for local, HTTP + OAuth for remote
- 📊 12 database tools — query, schema discovery, monitoring
- ⚡ Connection pooling with cursor-based result limiting
Try it now: npx @zlash65/postgresql-ssh-mcp
Architecture
Local: Claude Desktop → STDIO → MCP Server → [SSH Tunnel] → PostgreSQL
Remote: Claude/ChatGPT → HTTPS → MCP HTTP Server → [SSH Tunnel] → PostgreSQL
Deployment Options
| Method | Client | Transport | Use Case |
|---|---|---|---|
| 1 | Claude Desktop | STDIO | Local development, direct DB access |
| 2 | Claude Desktop | HTTP + OAuth | Remote DB via Connectors |
| 3 | ChatGPT | HTTP + OAuth | Remote DB via Developer Mode |
Method 1: Claude Desktop (Local STDIO)
Best for: Local development, databases accessible from your machine
Step 1: Find Config File
| Platform | Location |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%/Claude/claude_desktop_config.json |
In Claude Desktop: Settings > Developer > Edit Config
Step 2: Add MCP Server
Basic (local database):
{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "@zlash65/postgresql-ssh-mcp"],
"env": {
"DATABASE_URI": "postgresql://user:password@localhost:5432/mydb"
}
}
}
}
Optional: With SSH Tunnel (production database)
{
"mcpServers": {
"postgres-prod": {
"command": "npx",
"args": ["-y", "@zlash65/postgresql-ssh-mcp"],
"env": {
"DATABASE_URI": "postgresql://dbuser:dbpass@db.internal:5432/mydb",
"SSH_ENABLED": "true",
"SSH_HOST": "bastion.example.com",
"SSH_USER": "ec2-user",
"SSH_PRIVATE_KEY_PATH": "/Users/you/.ssh/id_rsa"
}
}
}
}
SSH_PRIVATE_KEY_PASSPHRASE, SSH_MAX_RECONNECT_ATTEMPTS
⚠️ Only enable for non-production databases.Enable Write Mode
{
"env": {
"DATABASE_URI": "postgresql://user:password@localhost:5432/mydb",
"READ_ONLY": "false"
}
}
Step 3: Restart Claude Desktop
Done! Ask Claude: "What tables are in my database?"
Remote HTTP Server Deployment
Best for: Shared team access, production databases, ChatGPT integration
This section covers deploying the MCP HTTP server with OAuth on a Linux server. Both Claude Desktop (via Connectors) and ChatGPT connect to the same server.
Prerequisites
- Linux server (Ubuntu 22.04/24.04 recommended)
- Domain name with DNS access
- Firewall allowing ports 22, 80, 443
- PostgreSQL database
Step 1: Configure DNS
Create an A record pointing your domain to your server's public IP:
| Type | Name | Value |
|---|---|---|
| A | your-subdomain | Server IP |
Verify: nslookup your-subdomain.example.com
Step 2: Install Dependencies
# Connect to server
ssh -i your-key.pem ubuntu@your-server-ip
# Update system
sudo apt update && sudo apt upgrade -y
# Install Node.js via nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
source ~/.bashrc
nvm install 22
# Install nginx and Certbot
sudo apt install -y nginx certbot python3-certbot-nginx
Step 3: Configure nginx
sudo vim /etc/nginx/sites-available/mcp
server {
listen 80;
server_name your-subdomain.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
}
Enable and reload:
sudo ln -s /etc/nginx/sites-available/mcp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Step 4: Obtain SSL Certificate
sudo certbot --nginx -d your-subdomain.example.com
# Verify auto-renewal
sudo certbot renew --dry-run
Step 5: Install MCP Server
sudo mkdir -p /opt/mcp-server
sudo chown ubuntu:ubuntu /opt/mcp-server
cd /opt/mcp-server
git clone https://github.com/zlash65/postgresql-ssh-mcp.git
cd postgresql-ssh-mcp
npm install && npm run build
Step 6: Configure Auth0
Your 6.1: Create Auth0 Tenant
postgresql-ssh-mcp)AUTH0_DOMAIN: {tenant-domain}.{region}.auth0.com
Step 7: Configure Environment
vim /opt/mcp-server/postgresql-ssh-mcp/.env
# Database
DATABASE_URI=postgresql://user:password@your-db-host:5432/your-database
DATABASE_SSL=false
READ_ONLY=true
# SSH Tunnel (optional)
# SSH_ENABLED=true
# SSH_HOST=bastion.example.com
# SSH_USER=ubuntu
# SSH_PRIVATE_KEY_PATH=/home/ubuntu/.ssh/id_rsa
# HTTP Server
MCP_HOST=0.0.0.0
# Auth0
MCP_AUTH_MODE=oauth
AUTH0_DOMAIN=your-tenant.us.auth0.com
AUTH0_AUDIENCE=https://your-subdomain.example.com/mcp
Step 8: Create systemd Service
sudo vim /etc/systemd/system/postgresql-ssh-mcp.service
[Unit]
Description=PostgreSQL SSH MCP Server
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/opt/mcp-server/postgresql-ssh-mcp
EnvironmentFile=/opt/mcp-server/postgresql-ssh-mcp/.env
Environment=PATH=/home/ubuntu/.nvm/versions/node/v22.21.1/bin:/usr/bin:/bin
ExecStart=/home/ubuntu/.nvm/versions/node/v22.21.1/bin/node /opt/mcp-server/postgresql-ssh-mcp/dist/http.js
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
Check your Node.js path with
which nodeand update accordingly.
sudo systemctl daemon-reload
sudo systemctl enable postgresql-ssh-mcp
sudo systemctl start postgresql-ssh-mcp
Step 9: Verify Deployment
# Check status
sudo systemctl status postgresql-ssh-mcp
# View logs
sudo journalctl -u postgresql-ssh-mcp -f
# Test health endpoint
curl https://your-subdomain.example.com/health
Expected response:
{"status": "ok", "timestamp": "...", "version": "1.x.x"}
Useful Commands
Command
Description
sudo systemctl restart postgresql-ssh-mcpRestart service
sudo journalctl -u postgresql-ssh-mcp -fLive logs
sudo nginx -t && sudo systemctl reload nginxReload nginx
sudo certbot renewRenew SSL
Method 2: Connect Claude Desktop (via Connectors)
- Open Claude Desktop Settings > Connectors
- Click on the Add custom connector button
- Click Add
- Authenticate with Auth0 (credentials from Step 6.6)
Method 3: Connect ChatGPT
- Profile > Settings > Apps > Enable Developer Mode
-
Create App:
- Name:
PostgreSQL SSH MCP - MCP Server URL:
https://your-subdomain.example.com - Authentication:
OAuth(leave Client ID/Secret empty)
- Name:
- Authenticate with Auth0 (credentials from Step 6.6)
- In chat: + > More > Select your MCP server
Available Tools
Query
| Tool | Description |
|---|---|
execute_query |
SQL with parameterized queries (results capped by MAX_ROWS) |
explain_query |
EXPLAIN plans in text/JSON/YAML/XML |
Schema
| Tool | Description |
|---|---|
list_schemas |
Database schemas (excludes system) |
list_tables |
Tables with row counts and sizes |
describe_table |
Columns, constraints, indexes |
list_databases |
All databases with sizes |
Monitoring
| Tool | Description |
|---|---|
get_connection_status |
Pool stats, tunnel state |
list_active_connections |
Active connections |
list_long_running_queries |
Slow queries |
get_database_version |
PostgreSQL version |
get_database_size |
Size breakdown |
get_table_stats |
Vacuum/analyze stats |
Security
Read-only by default — The server blocks INSERT, UPDATE, DELETE, DROP, TRUNCATE, ALTER, CREATE, GRANT, REVOKE, even SELECT INTO and PREPARE/EXECUTE.
SSH Host Key Verification — Trust-on-First-Use (TOFU) saves unknown keys on first connection and verifies subsequent connections. Disable with SSH_TRUST_ON_FIRST_USE=false.
Connection string obfuscation — Passwords are redacted in logs: postgresql://user:***@host:5432/mydb
Query limits — MAX_ROWS=1000, QUERY_TIMEOUT=30000, MAX_CONCURRENT_QUERIES=10
CORS/Origin validation (HTTP mode):
MCP_ALLOWED_ORIGINS="https://chatgpt.com,https://chat.openai.com"
MCP_ALLOWED_HOSTS="your-subdomain.example.com"
Environment Variables Reference
# Database
DATABASE_URI="postgresql://user:pass@host:5432/db"
DATABASE_SSL="true"
DATABASE_SSL_CA="/path/to/ca.pem"
# SSH Tunnel
SSH_ENABLED="true"
SSH_HOST="bastion.example.com"
SSH_USER="ec2-user"
SSH_PRIVATE_KEY_PATH="/path/to/key"
SSH_TRUST_ON_FIRST_USE="true"
SSH_MAX_RECONNECT_ATTEMPTS="5"
# Query
READ_ONLY="true"
MAX_ROWS="1000"
QUERY_TIMEOUT="30000"
# HTTP Server
PORT="3000"
MCP_AUTH_MODE="oauth"
AUTH0_DOMAIN="tenant.us.auth0.com"
AUTH0_AUDIENCE="https://your-domain.com"
Troubleshooting
Check logs: Common issues: Invalid Claude Desktop: Server Not Starting
~/Library/Logs/Claude/mcp*.log
%APPDATA%/Claude/logs/mcp*.log
DATABASE_URI, SSH key permissions (should be 600), PostgreSQL not accessible.
Verify Auth0: Default Audience set, DCR enabled, database connection promoted.ChatGPT: "Unable to connect"
# Verify server
curl https://your-subdomain.example.com/health
# Check OAuth metadata
curl https://your-subdomain.example.com/.well-known/oauth-protected-resource
401 Unauthorized
AUTH0_AUDIENCE must match API Identifier exactly












Top comments (0)