Microsoft SQL Server MCP Server (MSSQL)

by dperussina
Verified
// lib/database.js - Database utilities import sql from 'mssql'; import dotenv from 'dotenv'; import { logger } from './logger.mjs'; dotenv.config(); // Database configuration const dbConfig = { user: process.env.DB_USER || 'sa', password: process.env.DB_PASSWORD || 'YourStrong@Passw0rd', server: process.env.DB_SERVER || 'localhost', database: process.env.DB_DATABASE || 'master', port: parseInt(process.env.DB_PORT) || 1433, options: { encrypt: process.env.DB_ENCRYPT === 'true', trustServerCertificate: process.env.DB_TRUST_SERVER_CERT !== 'false', connectionTimeout: parseInt(process.env.DB_CONNECTION_TIMEOUT) || 15000, requestTimeout: parseInt(process.env.DB_REQUEST_TIMEOUT) || 15000, pool: { max: parseInt(process.env.DB_POOL_MAX) || 10, min: parseInt(process.env.DB_POOL_MIN) || 0, idleTimeoutMillis: parseInt(process.env.DB_POOL_IDLE_TIMEOUT) || 30000 } } }; // Global SQL pool let sqlPool = null; /** * Initialize the SQL connection pool * @returns {Promise<boolean>} - True if successful */ export async function initializeDbPool() { try { logger.info('Initializing SQL Server connection pool...'); // Create and connect the pool sqlPool = await new sql.ConnectionPool(dbConfig).connect(); // Setup pool error handler sqlPool.on('error', err => { logger.error(`SQL Pool Error: ${err.message}`); }); logger.info(`SQL Server connection pool initialized successfully (${dbConfig.server}/${dbConfig.database})`); return true; } catch (err) { logger.error(`Failed to initialize SQL Server connection pool: ${err.message}`); throw err; } } /** * Check if the SQL pool is connected and initialize if necessary * @returns {Promise<void>} */ async function ensurePoolConnected() { if (!sqlPool) { await initializeDbPool(); } else if (!sqlPool.connected) { logger.warn('SQL Pool disconnected, reconnecting...'); try { await sqlPool.connect(); } catch (err) { logger.error(`Failed to reconnect SQL pool: ${err.message}`); // Create a new pool if reconnect fails sqlPool = null; await initializeDbPool(); } } } /** * Execute a SQL query with retry logic * @param {string} sqlQuery - SQL query to execute * @param {object} parameters - Query parameters * @param {number} retryCount - Number of retries on transient errors * @returns {Promise<object>} - Query result */ export async function executeQuery(sqlQuery, parameters = {}, retryCount = 3) { if (sqlQuery.length > 100) { logger.info(`Executing SQL: ${sqlQuery.substring(0, 100)}...`); } else { logger.info(`Executing SQL: ${sqlQuery}`); } await ensurePoolConnected(); try { const request = sqlPool.request(); // Add parameters if provided for (const [key, value] of Object.entries(parameters)) { request.input(key, value); } const startTime = Date.now(); const result = await request.query(sqlQuery); const executionTime = Date.now() - startTime; logger.info(`SQL executed successfully in ${executionTime}ms, returned ${result.recordset?.length || 0} rows`); // Add execution time to result result.executionTime = executionTime; return result; } catch (err) { logger.error(`SQL execution failed: ${err.message}`); // Handle transient errors with retry logic const transientErrors = ['ETIMEOUT', 'ECONNCLOSED', 'ECONNRESET', 'ESOCKET']; if (transientErrors.includes(err.code) && retryCount > 0) { logger.info(`Retrying SQL execution (${retryCount} attempts left)...`); // Wait before retrying await new Promise(resolve => setTimeout(resolve, 1000)); // Force pool reconnection for connection-related errors if (['ECONNCLOSED', 'ECONNRESET'].includes(err.code)) { sqlPool = null; } return executeQuery(sqlQuery, parameters, retryCount - 1); } throw err; } } /** * Execute multiple SQL queries in a transaction * @param {Array<{sql: string, parameters: object}>} queries - Array of queries * @returns {Promise<Array<object>>} - Array of results */ export async function executeTransaction(queries) { if (!Array.isArray(queries) || queries.length === 0) { throw new Error('No queries provided for transaction'); } logger.info(`Starting transaction with ${queries.length} queries`); await ensurePoolConnected(); const transaction = new sql.Transaction(sqlPool); try { await transaction.begin(); logger.info('Transaction started'); const results = []; for (let i = 0; i < queries.length; i++) { const { sql: sqlQuery, parameters = {} } = queries[i]; logger.info(`Executing transaction query ${i + 1}/${queries.length}`); const request = new sql.Request(transaction); // Add parameters if provided for (const [key, value] of Object.entries(parameters)) { request.input(key, value); } const result = await request.query(sqlQuery); results.push(result); } await transaction.commit(); logger.info('Transaction committed successfully'); return results; } catch (err) { logger.error(`Transaction failed: ${err.message}`); // Try to roll back the transaction try { await transaction.rollback(); logger.info('Transaction rolled back'); } catch (rollbackErr) { logger.error(`Failed to roll back transaction: ${rollbackErr.message}`); } throw err; } } /** * Check if a table exists in the database * @param {string} tableName - Table name to check * @returns {Promise<boolean>} - True if table exists */ export async function tableExists(tableName) { try { const result = await executeQuery(` SELECT COUNT(*) AS TableCount FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @tableName `, { tableName }); return result.recordset[0].TableCount > 0; } catch (err) { logger.error(`Error checking if table exists: ${err.message}`); return false; } } /** * Sanitize SQL identifier to prevent SQL injection * @param {string} identifier - Identifier to sanitize * @returns {string} - Sanitized identifier */ export function sanitizeSqlIdentifier(identifier) { if (!identifier) return ''; // Remove brackets if present identifier = identifier.replace(/^\[|\]$/g, ''); // Remove SQL injection characters and non-alphanumeric characters return identifier.replace(/[^a-zA-Z0-9_]/g, ''); } /** * Get database configuration with optional password masking * @param {boolean} maskPassword - Whether to mask the password * @returns {object} - Database configuration */ export function getDbConfig(maskPassword = false) { const config = { ...dbConfig }; if (maskPassword) { config.password = '********'; } return config; } /** * Format SQL error for human-readable output * @param {Error} error - SQL error * @returns {string} - Formatted error message */ export function formatSqlError(error) { if (!error) return 'Unknown error'; // Special handling for SQL Server errors if (error.number) { return `SQL Error ${error.number}: ${error.message}`; } return error.message || 'Unknown SQL error'; }