Build an MCP server that gives AI safe, read-only access to your PostgreSQL database.
The server
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import pg from 'pg';
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const server = new McpServer({ name: 'database', version: '1.0.0' });
server.tool('query', {
sql: z.string().max(2000).describe('SQL SELECT query')
}, async ({ sql }) => {
const normalized = sql.trim().toUpperCase();
if (!normalized.startsWith('SELECT')) {
return { content: [{ type: 'text', text: 'Error: Only SELECT queries allowed' }], isError: true };
}
if (/\b(DROP|DELETE|UPDATE|INSERT|ALTER|TRUNCATE|CREATE)\b/.test(normalized)) {
return { content: [{ type: 'text', text: 'Error: Destructive operations not allowed' }], isError: true };
}
const result = await pool.query(sql);
return { content: [{ type: 'text', text: JSON.stringify(result.rows, null, 2) }] };
});
server.tool('list_tables', {}, async () => {
const result = await pool.query(
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'"
);
return { content: [{ type: 'text', text: result.rows.map(r => r.table_name).join('\n') }] };
});
await server.connect(new StdioServerTransport());
Connect
claude mcp add db node /path/to/db-server.js -e DATABASE_URL=postgresql://...
Now ask Claude: “Show me all users who signed up this week” or “What tables exist?”
Security
- Read-only credentials — create a Postgres user with SELECT-only permissions
- Query validation — block destructive operations at the application level
- Row limits — add
LIMIT 100to prevent full table dumps - GDPR — if the database contains PII, consider self-hosting the entire stack
See our MCP Security Checklist and PostgreSQL Cheat Sheet.
Related: Build MCP Server (TypeScript) · MCP Security Risks · Best MCP Servers