UNPKG

dj-postgres-mcp

Version:

Model Context Protocol (MCP) server for PostgreSQL database operations with centralized configuration via dj-config-mcp

333 lines (327 loc) • 13.2 kB
/** * ConfigHandler.ts - Configuration management for PostgreSQL MCP server * * This handler provides tools for configuring database connections. */ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { Client } from 'pg'; import { log, logError } from '../features/shared/logger.js'; import { MessageFormatter } from '../features/shared/message-formatter.js'; // Import the dj-config-mcp library using dynamic import let djConfig; // Initialize dj-config-mcp async function initializeDjConfig() { try { const module = await import('dj-config-mcp'); djConfig = module.default || module; log('ConfigHandler: dj-config-mcp initialized'); } catch (error) { logError('ConfigHandler: Failed to import dj-config-mcp', error); throw new Error('dj-config-mcp is required but not installed. Please install it with: npm install dj-config-mcp'); } } /** * ConfigHandler class manages PostgreSQL configuration and client setup */ export class ConfigHandler { config; djConfigInitPromise = null; constructor() { log('ConfigHandler: Constructor called'); this.config = this.getDefaultConfig(); this.initializeDjConfigAsync(); } async initializeDjConfigAsync() { if (!this.djConfigInitPromise) { this.djConfigInitPromise = initializeDjConfig(); } await this.djConfigInitPromise; } /** * Get default PostgreSQL configuration from environment variables */ getDefaultConfig() { return { host: process.env.POSTGRES_HOST || 'localhost', port: parseInt(process.env.POSTGRES_PORT || '5432'), database: process.env.POSTGRES_DATABASE || 'postgres', user: process.env.POSTGRES_USER || 'postgres', password: process.env.POSTGRES_PASSWORD || '', ssl: process.env.POSTGRES_SSL === 'true', connectionTimeout: 30000, queryTimeout: 60000 }; } /** * Gets the list of tools provided by this handler */ getTools() { return [ { name: 'configure_connection', description: 'Configure PostgreSQL database connection settings', inputSchema: { type: 'object', properties: { host: { type: 'string', description: 'Database host (e.g., localhost or Azure hostname)' }, port: { type: 'number', description: 'Database port (default: 5432)' }, database: { type: 'string', description: 'Database name' }, user: { type: 'string', description: 'Database username' }, password: { type: 'string', description: 'Database password' }, ssl: { type: 'boolean', description: 'Enable SSL connection (required for cloud databases)' }, }, required: ['host', 'database', 'user', 'password'] } }, { name: 'get_connection_info', description: 'Get current database connection configuration', inputSchema: { type: 'object', properties: {} } }, { name: 'test_connection', description: 'Test the current database connection', inputSchema: { type: 'object', properties: {} } } ]; } /** * Handles tool calls for this handler */ async handleToolCall(toolName, args) { log('ConfigHandler.handleToolCall: Tool call received', { toolName, hasArgs: !!args }); switch (toolName) { case 'configure_connection': return await this.handleConfigConnection(args); case 'get_connection_info': return await this.handleGetConnectionInfo(); case 'test_connection': return await this.handleTestConnection(); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`); } } /** * Handle database connection configuration */ async handleConfigConnection(args) { try { const { host, port, database, user, password, ssl } = args; // Validate required parameters if (!host || !database || !user || !password) { throw new McpError(ErrorCode.InvalidParams, MessageFormatter.error.validation('parameters', 'host, database, user, and password are required')); } // Update configuration this.config = { host, port: port || 5432, database, user, password, ssl: ssl !== undefined ? ssl : true, connectionTimeout: 30000, queryTimeout: 60000 }; // Store configuration using dj-config-mcp await this.initializeDjConfigAsync(); // Store configuration values using dj-config-mcp // dj-config-mcp will automatically detect password as sensitive and store in .env await djConfig.configSet('postgres.host', host); await djConfig.configSet('postgres.port', (port || 5432).toString()); await djConfig.configSet('postgres.database', database); await djConfig.configSet('postgres.user', user); await djConfig.configSet('postgres.password', password); await djConfig.configSet('postgres.ssl', (ssl !== undefined ? ssl : true).toString()); log('ConfigHandler: Configuration saved using dj-config-mcp'); // Test the connection const testResult = await this.testConnectionInternal(); let statusMessage = ''; if (testResult.success) { statusMessage = MessageFormatter.success.connection(`- Server Version: ${testResult.version}\n- Connection Latency: ${testResult.latency}ms`); } else { statusMessage = MessageFormatter.warning.general(`Connection test failed: ${testResult.error}`); } return { content: [{ type: 'text', text: `${MessageFormatter.headers.configuration()}\n\n${MessageFormatter.info.configuration(this.config.host, this.config.port, this.config.database, this.config.user, this.config.ssl)}\n\n${statusMessage}\n\n${MessageFormatter.info.general('Configuration saved using dj-config-mcp')}` }] }; } catch (error) { logError('ConfigHandler.handleConfigConnection: Error', error); if (error instanceof McpError) { throw error; } return { content: [{ type: 'text', text: MessageFormatter.error.configuration(String(error)) }], isError: true }; } } /** * Handle get connection info */ async handleGetConnectionInfo() { await this.initializeDjConfigAsync(); // Load configuration from dj-config-mcp const hostConfig = await djConfig.configGet('postgres.host'); const portConfig = await djConfig.configGet('postgres.port'); const databaseConfig = await djConfig.configGet('postgres.database'); const userConfig = await djConfig.configGet('postgres.user'); const sslConfig = await djConfig.configGet('postgres.ssl'); const passwordConfig = await djConfig.configGet('postgres.password'); const host = hostConfig?.value || this.config.host; const port = parseInt(portConfig?.value || this.config.port.toString()); const database = databaseConfig?.value || this.config.database; const user = userConfig?.value || this.config.user; const ssl = (sslConfig?.value || this.config.ssl.toString()) === 'true'; const passwordConfigured = !!(passwordConfig?.value || this.config.password); const configInfo = { host, port, database, user, ssl, passwordConfigured // Use camelCase for consistency with other app-level fields }; return { content: [{ type: 'text', text: `šŸ“‹ Current PostgreSQL Configuration ${JSON.stringify(configInfo, null, 2)} šŸ’” Configuration managed by dj-config-mcp` }] }; } /** * Handle test connection */ async handleTestConnection() { const result = await this.testConnectionInternal(); if (result.success) { return { content: [{ type: 'text', text: `āœ… Connection Test Successful! Connection Details: - Host: ${this.config.host}:${this.config.port} - Database: ${this.config.database} - User: ${this.config.user} - SSL: ${this.config.ssl ? 'enabled' : 'disabled'} - Server Version: ${result.version} - Connection Latency: ${result.latency}ms` }] }; } else { return { content: [{ type: 'text', text: `āŒ Connection Test Failed Error: ${result.error} Please check your connection settings using 'configure_connection'` }], isError: true }; } } /** * Test database connection internally */ async testConnectionInternal() { const startTime = Date.now(); try { // Load configuration from dj-config-mcp if available if (this.djConfigInitPromise) { const hostConfig = await djConfig.configGet('postgres.host'); const portConfig = await djConfig.configGet('postgres.port'); const databaseConfig = await djConfig.configGet('postgres.database'); const userConfig = await djConfig.configGet('postgres.user'); const passwordConfig = await djConfig.configGet('postgres.password'); const sslConfig = await djConfig.configGet('postgres.ssl'); if (hostConfig?.value) this.config.host = hostConfig.value; if (portConfig?.value) this.config.port = parseInt(portConfig.value); if (databaseConfig?.value) this.config.database = databaseConfig.value; if (userConfig?.value) this.config.user = userConfig.value; if (passwordConfig?.value) this.config.password = passwordConfig.value; if (sslConfig?.value !== null && sslConfig?.value !== undefined) this.config.ssl = sslConfig.value === 'true'; } const client = new Client({ host: this.config.host, port: this.config.port, database: this.config.database, user: this.config.user, password: this.config.password, ssl: this.config.ssl ? { rejectUnauthorized: false } : false, connectionTimeoutMillis: this.config.connectionTimeout, query_timeout: this.config.queryTimeout }); await client.connect(); const result = await client.query('SELECT version() as version'); const latency = Date.now() - startTime; await client.end(); return { success: true, version: result.rows[0].version, latency: latency }; } catch (error) { logError('ConfigHandler.testConnectionInternal: Connection test failed', error); const errorMessage = error instanceof Error ? error.message : String(error); return { success: false, error: errorMessage || 'Unknown connection error', latency: Date.now() - startTime }; } } /** * Get current configuration */ getConfig() { return { ...this.config }; } /** * Check if database connection is configured */ isConfigured() { return !!(this.config.host && this.config.database && this.config.user && this.config.password); } }