Authentication Guide
The MCP server includes built-in authentication with two methods: API tokens for machine-to-machine communication and user accounts for interactive authentication.
- Use an API Token for direct machine-to-machine access. Tokens are long-lived and easily managed by administrators.
-
Use a User Account for interactive applications; an account is session-based, and users can manage own password access.
-
API Tokens: For machine-to-machine communication (direct HTTP/HTTPS access)
- User Accounts: For interactive authentication with session tokens
- Enabled by default in HTTP/HTTPS mode
- SHA256/Bcrypt hashing for secure credential storage
- Token expiration with automatic cleanup
- Per-token connection isolation for multi-user security
- Bearer token authentication using HTTP Authorization header
- Auto-reload of token and user files without server restart
- Rate limiting: Per-IP protection against brute force attacks
- Account lockout: Automatic account disabling after failed attempts
- Not required for stdio mode (Claude Desktop)
When configuring authentication:
- test your authentication in development, and verify file edits before any production changes.
- monitor your logfiles, watching for reload confirmations and errors.
- use tools that write atomically (most editors do) so you don't lose edits.
- keep backups before making any major changes or bulk edits.
- use
-list-tokensor-list-usersto confirm that authentication changes are performing as expected.
Note: The /mcp/v1 endpoint requires authentication (unless -no-auth is specified during endpoint configuration):
# Without token - returns 401 Unauthorized
curl -X POST http://localhost:8080/mcp/v1 \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}'
# Response:
{"error": "Unauthorized"}
Rate Limiting and Account Lockout
The MCP server includes built-in protection against brute force attacks through per-IP rate limiting and automatic account lockout. When a valid username is provided, the MCP server tracks the number of failed login attempts for the account and locks that account if authentication is not successful.
Automatic lockout disables an account after a specified number of consecutive failed attempts. The configurable threshold allows you to specify the maximum failed attempts (default: 0 = disabled). An administrator can** use the -enable-user CLI command to re-enable locked accounts.
Failed authentication attempts are tracked per IP address to prevent brute force attacks:
- By default, 10 failed attempts per 15-minute window per IP address invokes a lockout.
- This value is configurable - you can customize both the time window and attempt limit.
- Automatic cleanup ensures that old attempts are automatically removed from memory.
- Lockout is status-blind - rate limiting applies regardless of whether the username exists.
Configuring Rate Limiting and Lockout
To configure lockout with a configuration file, add these properties to the file:
http:
auth:
enabled: true
token_file: "./postgres-mcp-tokens.yaml"
# Rate limiting settings
rate_limit_window_minutes: 15 # Time window for rate limiting
rate_limit_max_attempts: 10 # Max attempts per IP per window
# Account lockout settings
max_failed_attempts_before_lockout: 5 # 0 = disabled
Example: Enabling Account Lockout
http:
auth:
enabled: true
token_file: "./postgres-mcp-tokens.yaml"
max_failed_attempts_before_lockout: 5
rate_limit_window_minutes: 15
rate_limit_max_attempts: 10
With this configuration:
- After 5 failed login attempts, the account will be automatically disabled.
- IP addresses are limited to 10 failed attempts per 15-minute window.
- The server logs show when rate limiting is enabled.
You can also configure lockout with the following environment variables:
export PGEDGE_AUTH_MAX_FAILED_ATTEMPTS_BEFORE_LOCKOUT=5
export PGEDGE_AUTH_RATE_LIMIT_WINDOW_MINUTES=15
export PGEDGE_AUTH_RATE_LIMIT_MAX_ATTEMPTS=10
Recovering a Locked Account
The following command enables a locked account:
# Re-enable a locked account
./bin/pgedge-postgres-mcp -enable-user -username alice
# Reset failed attempts counter
# (automatically reset on successful login)
Automatic File Reloading
The MCP server automatically detects and reloads changes to token and user files without requiring a server restart. This enables hot updates to authentication credentials while the server is running.
The server uses file system notifications (via fsnotify) to monitor the
token and user files for changes. When a file is modified, the server
automatically reloads the credentials:
- Instant updates: Changes take effect within 100ms
- No downtime: Server continues running during reload
- Thread-safe: Uses read-write locks to prevent race conditions
- Editor-friendly: Handles file deletion/recreation during saves
- Session preservation: Active user sessions remain valid during reload
- Debouncing: Batches rapid file changes to avoid excessive reloads
The server watches the directory containing the auth files (not the files directly) because many editors delete and recreate files when saving. This ensures that the watcher continues working after file edits.
During the reload process:
- File system event detected (Write or Create)
- Debounce timer (100ms) starts to batch rapid changes
- Reload function executes with write lock
- New credentials loaded from disk
- Old credentials replaced atomically
- Active sessions preserved (for user files)
- Confirmation logged to server output
Thread Safety
All reload operations use read-write locks (sync.RWMutex) to ensure:
- Multiple concurrent read operations (authentication checks) can proceed
- Write operations (reloads) block all other operations temporarily
- No race conditions between authentication and reload
- Atomic replacement of credential data
Monitoring Reload Events
Reload events are added to the server logs as shown below:
[AUTH] Reloaded /path/to/postgres-mcp-tokens.yaml
[AUTH] Reloaded /path/to/postgres-mcp-users.yaml
Failed reloads are also logged:
[AUTH] Failed to reload /path/to/postgres-mcp-tokens.yaml:
permission denied
Auto-Reload Limitations
- File must exist: Deleting the file entirely will cause errors.
- Valid YAML required: Syntax errors can prevent files from reloading (old data is retained).
- Same location: Moving the file to a different location requires a restart.
- No cascade: Changing the token file path in your configuration requires a restart.
Implementation
The auto-reload feature is implemented using:
- fsnotify: Cross-platform file system notifications
- Watcher goroutine: Background monitoring in separate thread
- Debounce timer: 100ms delay to batch rapid changes
- RWMutex locks: Thread-safe data structure access
- Reload callbacks: TokenStore.Reload() and UserStore.Reload()
For implementation details, see:
- internal/auth/watcher.go - File watching
- internal/auth/auth.go - Token store reload
- internal/auth/users.go - User store reload
Authentication Flow
For an interactive application that uses password authentication, authentication follows a two-step process. During authentication, the user authenticates, and is then assigned a token. That token is used for secure authentication for the session:
Step 1: Authenticate the User with a Password
Call the authenticate_user tool (this tool is NOT advertised to the LLM and is only for direct client use):
curl -X POST http://localhost:8080/mcp/v1 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "authenticate_user",
"arguments": {
"username": "alice",
"password": "SecurePassword123!"
}
}
}'
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\"success\": true, \"session_token\": \"AQz9XfK...\", \"expires_at\": \"2024-11-15T09:30:00Z\", \"message\": \"Authentication successful\"}"
}
]
}
}
The returned token is:
- valid for 24 hours from authentication
- a Base64-encoded random 32-byte token
- strongly random, cryptographically secure
After 24 hours, the user is required to re-authenticate to get a new session token.
Step 2: Use a Session Token to Authenticate
Extract the session_token from the response and use it as a Bearer token for all subsequent requests:
curl -X POST http://localhost:8080/mcp/v1 \
-H "Authorization: Bearer AQz9XfK..." \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "query_database",
"arguments": {
"query": "Show me all users"
}
}
}'
Authorization: Database Access Control
The MCP server enforces authorization through the
available_to_users field on each database configuration.
This field restricts which authenticated users can access
each database connection.
Configuring User Access
The available_to_users field accepts a list of usernames.
An empty list grants access to all authenticated users.
In the following example, the configuration defines three databases with different access levels:
databases:
- name: "production"
host: "prod-db.example.com"
port: 5432
database: "myapp"
user: "readonly_user"
sslmode: "require"
available_to_users: [] # All users
- name: "staging"
host: "staging-db.example.com"
port: 5432
database: "myapp_staging"
user: "developer"
available_to_users:
- "alice"
- "bob"
- name: "development"
host: "localhost"
port: 5432
database: "myapp_dev"
user: "developer"
available_to_users:
- "alice"
- "bob"
- "charlie"
The following table shows database access for each user with the configuration above:
| User | production | staging | development |
|---|---|---|---|
| alice | Yes | Yes | Yes |
| bob | Yes | Yes | Yes |
| charlie | Yes | No | Yes |
| dave | Yes | No | No |
Per-Token Database Binding
API tokens can bind to a specific database using the
-token-database flag. A bound token cannot switch to
another database during its lifetime.
In the following example, the -add-token command creates
a token bound to the production database:
./bin/pgedge-postgres-mcp -add-token \
-token-note "Production Monitor" \
-token-database "production" \
-token-expiry "90d"
Authorization Model Summary
The server combines authentication and authorization to control database access:
- User authentication and the
available_to_usersfield determine which databases a user can access. - Token authentication and the
-token-databaseflag determine which database a token can reach. - An empty
available_to_userslist allows all authenticated users to access the database. - API tokens bound to a database cannot switch to another database.
For more information about managing multiple databases, see the Multiple Database Configuration guide.
Client Implementation Example
The following example demonstrates implementing authentication in a Python client.
import requests
import json
class MCPUserClient:
def __init__(self, base_url):
self.base_url = base_url
self.session_token = None
self.token_expiry = None
def authenticate(self, username, password):
"""Authenticate and get session token"""
response = requests.post(
f"{self.base_url}/mcp/v1",
json={
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "authenticate_user",
"arguments": {
"username": username,
"password": password
}
}
}
)
result = response.json()
if "result" in result:
auth_data = json.loads(result["result"]["content"][0]["text"])
self.session_token = auth_data["session_token"]
self.token_expiry = auth_data["expires_at"]
return True
return False
def call_tool(self, tool_name, arguments):
"""Call a tool using the session token"""
if not self.session_token:
raise Exception("Not authenticated")
response = requests.post(
f"{self.base_url}/mcp/v1",
headers={
"Authorization": f"Bearer {self.session_token}",
"Content-Type": "application/json"
},
json={
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": tool_name,
"arguments": arguments
}
}
)
return response.json()
# Usage
client = MCPUserClient("http://localhost:8080")
if client.authenticate("alice", "SecurePassword123!"):
result = client.call_tool("query_database", {"query": "Show tables"})
print(result)
Token Lifecycle Management
The MCP server manages tokens through creation, validation, and expiration. Understanding the token lifecycle helps clients maintain uninterrupted access.
Token Creation
The server supports two methods for creating tokens:
- The
-add-tokenCLI command creates long-lived API tokens for machine-to-machine access. - The
authenticate_usertool creates session tokens for interactive user authentication. - Session tokens remain valid for 24 hours after the server creates them.
Token Validation
The server validates every token before processing a request. The validation checks the token format, existence, and expiration timestamp.
- The server validates the token on every request.
- Expired tokens receive a
401 Unauthorizedresponse. - Invalid tokens also receive a
401 Unauthorizedresponse.
Detecting Token Expiration
Clients can check token expiry before sending a request.
This approach avoids unnecessary 401 responses from
the server.
In the following example, the is_token_expired function
checks a token expiry timestamp against the current time:
from datetime import datetime, timezone
def is_token_expired(expiry_str):
"""Check if a token has expired."""
expiry = datetime.fromisoformat(
expiry_str.replace("Z", "+00:00")
)
return datetime.now(timezone.utc) >= expiry
# Check before making a request
if is_token_expired(client.token_expiry):
client.authenticate(username, password)
Automatic Re-Authentication
Clients should handle expired tokens by catching 401
errors and re-authenticating automatically. This pattern
ensures seamless recovery from token expiration.
In the following example, the call_with_retry function
re-authenticates when the server returns a 401 error:
def call_with_retry(client, tool_name, arguments,
username, password):
"""Call a tool with automatic re-authentication."""
try:
return client.call_tool(tool_name, arguments)
except Exception as e:
if "401" in str(e) or "Unauthorized" in str(e):
client.authenticate(username, password)
return client.call_tool(
tool_name, arguments
)
raise
Mid-Conversation Token Expiration
A token can expire while a conversation is in progress. The server handles this situation gracefully without losing context.
- The next request receives a
401response from the server. - The client should re-authenticate and retry the failed request.
- The server preserves previous conversation context during re-authentication.
- No data is lost during the re-authentication process.
Best Practices for Programmatic Clients
Programmatic clients should follow these security and reliability practices for token management:
- Never log tokens or include tokens in error messages.
- Use environment variables to store credentials instead of hardcoding them.
- Store session tokens in memory rather than writing them to disk.
- Implement token rotation for long-lived background processes.
- Check the token expiry before each request to avoid
unnecessary
401errors. - Set reasonable timeout values for all API requests.
For working client examples, see the Client Examples page. For token administration commands, see the Token Management page.
Updating Passwords and Tokens
You can use the following command to update a user password:
# Server running with active user sessions
# Update user password
./bin/pgedge-postgres-mcp -update-user \
-username alice \
-password "NewSecurePassword456!"
# Server reloads user file
# Alice's active session remains valid
# New password required for next login
To perform a bulk update of session tokens, you can edit the token file directly:
# Edit token file directly for bulk changes
nano postgres-mcp-tokens.yaml
# On save, server automatically detects change:
# [AUTH] Reloaded /path/to/postgres-mcp-tokens.yaml
Error Responses
The following responses may occur as a result of authentication errors:
| Error Type | JSON Response | HTTP Status |
|---|---|---|
| Missing Token | { "error": "Unauthorized" } |
401 Unauthorized |
| Invalid Token | { "error": "Unauthorized" } |
401 Unauthorized |
| Expired Token | { "error": "Unauthorized" } |
401 Unauthorized |
Note: For security reasons, specific error details are not exposed.
Health Endpoint
The /health endpoint is always accessible without authentication:
# No token required
curl http://localhost:8080/health
To Disable Authentication (Development Only)
Warning
Never disable authentication in production!
The following command disables authentication:
./bin/pgedge-postgres-mcp -http -no-auth
Security Considerations
See the Security Guide for comprehensive security best practices related to:
- Token storage and protection
- HTTPS requirements
- Network security
- Audit logging
- Incident response