UNPKG

@henkey/postgres-mcp-server

Version:

A Model Context Protocol (MCP) server that provides comprehensive PostgreSQL database management capabilities for AI assistants

153 lines 6.27 kB
import { DatabaseConnection } from '../utils/connection.js'; import { analyzeDatabase as originalAnalyzeDatabase } from './analyze.js'; // Assuming it's from a .js file initially import { z } from 'zod'; // Definition previously in TOOL_DEFINITIONS const toolDefinition = { name: 'pg_analyze_database', description: 'Analyze PostgreSQL database configuration and performance', inputSchema: z.object({ connectionString: z.string().optional() .describe('PostgreSQL connection string (optional if POSTGRES_CONNECTION_STRING environment variable or --connection-string CLI option is set)'), analysisType: z.enum(['configuration', 'performance', 'security']).optional() .describe('Type of analysis to perform') }) }; export const analyzeDatabaseTool = { name: toolDefinition.name, description: toolDefinition.description, inputSchema: toolDefinition.inputSchema, execute: async (args, getConnectionString) => { const { connectionString: connStringArg, analysisType } = args; if (!analysisType || !['configuration', 'performance', 'security'].includes(analysisType)) { return { content: [{ type: 'text', text: 'Error: analysisType is required and must be one of [\'configuration\', \'performance\', \'security\'].' }], isError: true, }; } const resolvedConnString = getConnectionString(connStringArg); const result = await originalAnalyzeDatabase(resolvedConnString, analysisType); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2) } ] }; }, }; export async function analyzeDatabase(connectionString, analysisType = 'configuration') { 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() { const db = DatabaseConnection.getInstance(); const result = await db.query('SELECT version()'); return result[0].version; } async function getSettings() { const db = DatabaseConnection.getInstance(); const result = await db.query('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, row) => { acc[row.name] = row.unit ? `${row.setting}${row.unit}` : row.setting; return acc; }, {}); } async function getMetrics() { const db = DatabaseConnection.getInstance(); const connections = await db.query('SELECT count(*) FROM pg_stat_activity'); const activeQueries = await db.query("SELECT count(*) FROM pg_stat_activity WHERE state = 'active'"); // First get raw stats for diagnostic logging const rawStats = await db.query(`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(`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; // If rawRatio is a string, parseFloat it. // If it's already a number, just convert using Number(). if (typeof rawRatio === 'string') { ratio = Number.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(`SELECT tablename, pg_size_pretty(pg_table_size(schemaname || '.' || tablename)) as size FROM pg_tables WHERE schemaname = 'public'`); return { connections: Number.parseInt(connections[0].count), activeQueries: Number.parseInt(activeQueries[0].count), cacheHitRatio: Number.parseFloat(ratio.toFixed(2)), tableSizes: tableSizes.reduce((acc, row) => { acc[row.tablename] = row.size; return acc; }, {}), }; } async function generateRecommendations(type, settings, metrics) { const recommendations = []; if (type === 'configuration' || type === 'performance') { if (metrics.cacheHitRatio < 0.99) { recommendations.push('Consider increasing shared_buffers to improve cache hit ratio'); } if (metrics.connections > Number.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("SELECT count(*) FROM pg_user WHERE usesuper = true"); if (Number.parseInt(superusers[0].count) > 1) { recommendations.push('Multiple superuser accounts detected. Review and reduce if possible'); } // Check SSL const ssl = await db.query("SHOW ssl"); if (ssl[0].ssl !== 'on') { recommendations.push('SSL is not enabled. Consider enabling SSL for secure connections'); } } return recommendations; } //# sourceMappingURL=analyze.js.map