PostgreSQL MCP Server
by nahmanmate
import { DatabaseConnection } from '../utils/connection.js';
interface AnalysisResult {
version: string;
settings: Record<string, any>;
metrics: {
connections: number;
activeQueries: number;
cacheHitRatio: number;
tableSizes: Record<string, string>;
};
recommendations: string[];
}
export async function analyzeDatabase(
connectionString: string,
analysisType: 'configuration' | 'performance' | 'security' = 'configuration'
): Promise<AnalysisResult> {
const db = DatabaseConnection.getInstance();
await db.connect(connectionString);
try {
const version = await getVersion();
const settings = await getSettings();
const metrics = await getMetrics();
const recommendations = await generateRecommendations(analysisType, settings, metrics);
return {
version,
settings,
metrics,
recommendations,
};
} finally {
await db.disconnect();
}
}
async function getVersion(): Promise<string> {
const db = DatabaseConnection.getInstance();
const result = await db.query<{ version: string }>('SELECT version()');
return result[0].version;
}
async function getSettings(): Promise<Record<string, any>> {
const db = DatabaseConnection.getInstance();
const result = await db.query<{ name: string; setting: string; unit: string }>(
'SELECT name, setting, unit FROM pg_settings WHERE name IN ($1, $2, $3, $4, $5)',
['max_connections', 'shared_buffers', 'work_mem', 'maintenance_work_mem', 'effective_cache_size']
);
return result.reduce((acc: Record<string, string>, row: { name: string; setting: string; unit: string }) => {
acc[row.name] = row.unit ? `${row.setting}${row.unit}` : row.setting;
return acc;
}, {});
}
async function getMetrics(): Promise<AnalysisResult['metrics']> {
const db = DatabaseConnection.getInstance();
const connections = await db.query<{ count: string }>(
'SELECT count(*) FROM pg_stat_activity'
);
const activeQueries = await db.query<{ count: string }>(
"SELECT count(*) FROM pg_stat_activity WHERE state = 'active'"
);
// First get raw stats for diagnostic logging
const rawStats = await db.query<{ datname: string; hits: number; reads: number }>(
`SELECT
datname,
COALESCE(blks_hit, 0) as hits,
COALESCE(blks_read, 0) as reads
FROM pg_stat_database
WHERE datname = current_database()`
);
console.error('Cache stats:', rawStats[0]); // Diagnostic logging
// Then calculate ratio with additional safety checks
const cacheHit = await db.query<{ ratio: number }>(
`WITH stats AS (
SELECT
COALESCE(blks_hit, 0) as hits,
COALESCE(blks_read, 0) as reads
FROM pg_stat_database
WHERE datname = current_database()
)
SELECT
CASE
WHEN (hits + reads) = 0 THEN 0
ELSE ROUND((hits::float / (hits + reads)::float)::numeric, 2)
END as ratio
FROM stats`
);
// Ensure ratio is a number
const rawRatio = cacheHit[0]?.ratio ?? 0;
let ratio: number;
// If rawRatio is a string, parseFloat it.
// If it's already a number, just convert using Number().
if (typeof rawRatio === 'string') {
ratio = parseFloat(rawRatio);
} else {
ratio = Number(rawRatio);
}
// Fallback to 0 if the result is NaN
if (Number.isNaN(ratio)) {
ratio = 0;
}
console.error('Calculated ratio:', ratio); // Diagnostic logging
const tableSizes = await db.query<{ tablename: string; size: string }>(
`SELECT
tablename,
pg_size_pretty(pg_table_size(schemaname || '.' || tablename)) as size
FROM pg_tables
WHERE schemaname = 'public'`
);
return {
connections: parseInt(connections[0].count),
activeQueries: parseInt(activeQueries[0].count),
cacheHitRatio: parseFloat(ratio.toFixed(2)),
tableSizes: tableSizes.reduce((acc: Record<string, string>, row: { tablename: string; size: string }) => {
acc[row.tablename] = row.size;
return acc;
}, {}),
};
}
async function generateRecommendations(
type: 'configuration' | 'performance' | 'security',
settings: Record<string, any>,
metrics: AnalysisResult['metrics']
): Promise<string[]> {
const recommendations: string[] = [];
if (type === 'configuration' || type === 'performance') {
if (metrics.cacheHitRatio < 0.99) {
recommendations.push('Consider increasing shared_buffers to improve cache hit ratio');
}
if (metrics.connections > parseInt(settings.max_connections) * 0.8) {
recommendations.push('High connection usage detected. Consider increasing max_connections or implementing connection pooling');
}
}
if (type === 'security') {
const db = DatabaseConnection.getInstance();
// Check for superusers
const superusers = await db.query<{ count: string }>(
"SELECT count(*) FROM pg_user WHERE usesuper = true"
);
if (parseInt(superusers[0].count) > 1) {
recommendations.push('Multiple superuser accounts detected. Review and reduce if possible');
}
// Check SSL
const ssl = await db.query<{ ssl: string }>("SHOW ssl");
if (ssl[0].ssl !== 'on') {
recommendations.push('SSL is not enabled. Consider enabling SSL for secure connections');
}
}
return recommendations;
}