crew-management-mcp-server
Version:
Crew management server handling crew records, certifications, scheduling, payroll, and vessel assignments with ERP access for data extraction
280 lines • 12.3 kB
JavaScript
import { getConfig } from './config.js';
import { logger } from './logger.js';
// Snowflake connection pool
let connectionPool = [];
const MAX_POOL_SIZE = 5;
export class SnowflakeClient {
constructor() {
this.connection = null;
}
async connect() {
logger.info('Connecting to Snowflake database...');
try {
// Get fresh configuration
const config = getConfig();
// Validate required configuration
if (!config.snowflakeAccount || !config.snowflakeUser || !config.snowflakePassword) {
throw new Error('Missing required Snowflake configuration: account, user, or password');
}
// Dynamic import for ES module compatibility
const snowflake = await import('snowflake-sdk');
const sdk = snowflake.default || snowflake;
const connectionOptions = {
account: config.snowflakeAccount,
username: config.snowflakeUser, // Note: SDK uses 'username', not 'user'
password: config.snowflakePassword,
application: 'crew-management-mcp-server',
timeout: 60000, // 60 seconds
// Add optional parameters only if they exist
...(config.snowflakeWarehouse && { warehouse: config.snowflakeWarehouse }),
...(config.snowflakeDatabase && { database: config.snowflakeDatabase }),
...(config.snowflakeSchema && { schema: config.snowflakeSchema }),
...(config.snowflakeRole && { role: config.snowflakeRole }),
};
return new Promise((resolve, reject) => {
try {
// Configure Snowflake SDK
sdk.configure({
logLevel: config.logLevel === 'debug' ? 'DEBUG' : 'INFO',
// Remove ocspFailOpen for compatibility
});
logger.info('Creating Snowflake connection with options:', {
account: connectionOptions.account,
username: connectionOptions.username,
warehouse: connectionOptions.warehouse,
database: connectionOptions.database,
schema: connectionOptions.schema
});
const connection = sdk.createConnection(connectionOptions);
connection.connect((err, conn) => {
if (err) {
logger.error('Unable to connect to Snowflake:', {
code: err.code,
message: err.message,
sqlState: err.sqlState,
data: err.data
});
reject(err);
}
else {
logger.info('Successfully connected to Snowflake', {
sessionId: conn.getSessionId?.(),
database: conn.getDatabase?.(),
schema: conn.getSchema?.(),
warehouse: conn.getWarehouse?.()
});
this.connection = conn;
resolve(conn);
}
});
}
catch (setupError) {
logger.error('Error setting up Snowflake connection:', setupError);
reject(setupError);
}
});
}
catch (error) {
logger.error('Error connecting to Snowflake:', error);
throw error;
}
}
async execute(query) {
if (!this.connection) {
await this.connect();
}
return new Promise((resolve, reject) => {
// Add timeout to prevent hanging
const timeoutId = setTimeout(() => {
logger.error('Snowflake query execution timeout after 30 seconds');
reject(new Error('Query execution timeout'));
}, 30000);
this.connection.execute({
sqlText: query,
complete: (err, stmt, rows) => {
clearTimeout(timeoutId);
if (err) {
logger.error('Error executing Snowflake query:', {
code: err.code,
message: err.message,
sqlState: err.sqlState,
query: query.substring(0, 200) + '...'
});
reject(err);
}
else {
try {
// Extract column names more robustly
let columns = [];
try {
const columnInfo = stmt.getColumns();
columns = columnInfo.map((col) => {
// Try different ways to get column name
return col.name || col.columnName || col.COLUMN_NAME || col.getName?.() || 'UNKNOWN_COLUMN';
});
logger.debug('Column extraction successful', {
columnCount: columns.length,
columnNames: columns.slice(0, 5) // Log first 5 column names
});
}
catch (columnError) {
logger.warn('Error extracting column names, using fallback', columnError);
// Fallback: try to infer from first row if available
if (rows && rows.length > 0 && typeof rows[0] === 'object') {
columns = Object.keys(rows[0]);
logger.debug('Using fallback column extraction from row data', { columns: columns.slice(0, 5) });
}
}
logger.debug('Snowflake query executed successfully', {
query: query.substring(0, 100) + '...',
rowCount: rows?.length || 0,
columnCount: columns.length,
hasColumns: columns.length > 0,
rowType: rows && rows.length > 0 ? typeof rows[0] : 'no data'
});
// Rows are already objects from Snowflake SDK by default
resolve({ columns, rows: rows || [] });
}
catch (resultError) {
logger.error('Error processing Snowflake query results:', resultError);
reject(resultError);
}
}
}
});
});
}
async close() {
if (this.connection) {
return new Promise((resolve, reject) => {
this.connection.destroy((err, conn) => {
if (err) {
logger.error('Error closing Snowflake connection:', err);
reject(err);
}
else {
logger.info('Snowflake connection closed successfully');
this.connection = null;
resolve();
}
});
});
}
}
}
export async function getConnectionFromPool() {
// Simple connection pooling
if (connectionPool.length > 0) {
return connectionPool.pop();
}
const client = new SnowflakeClient();
await client.connect();
return client;
}
export async function returnConnectionToPool(client) {
if (connectionPool.length < MAX_POOL_SIZE) {
connectionPool.push(client);
}
else {
await client.close();
}
}
export async function closeAllConnections() {
const closePromises = connectionPool.map(client => client.close());
await Promise.all(closePromises);
connectionPool = [];
logger.info('All Snowflake connections closed');
}
export async function testConnection() {
try {
logger.info('Starting Snowflake connection test...');
const client = new SnowflakeClient();
logger.info('Establishing connection...');
await client.connect();
logger.info('Executing test query...');
const result = await client.execute('SELECT CURRENT_VERSION() as version');
logger.info('Snowflake connection test successful', {
version: result.rows[0]?.[0],
rowCount: result.rows.length
});
logger.info('Closing test connection...');
await client.close();
return true;
}
catch (error) {
logger.error('Snowflake connection test failed:', {
message: error instanceof Error ? error.message : 'Unknown error',
code: error?.code,
sqlState: error?.sqlState
});
return false;
}
}
export async function getSeafarerDetailsFromSnowflake(crewIds, requiredFields) {
const allResults = [];
if (!crewIds || crewIds.length === 0) {
logger.warn('No crew IDs provided for Snowflake query');
return allResults;
}
try {
const client = await getConnectionFromPool();
try {
// Process in batches for better performance
const batchSize = 10;
for (let i = 0; i < crewIds.length; i += batchSize) {
const batch = crewIds.slice(i, i + batchSize);
for (const id of batch) {
try {
const query = `
WITH base AS (
SELECT
${requiredFields}
FROM revised_base_view
WHERE CREW_CODE = ?
)
SELECT *
FROM base
ORDER BY SIGN_ON_DATE DESC
LIMIT 1;
`;
// Note: For parameterized queries, we need to format differently
// This is a simplified version - consider using proper parameterization
const formattedQuery = query.replace('?', `'${id.replace(/'/g, "''")}'`);
const result = await client.execute(formattedQuery);
const results = result.rows.map((row) => {
const obj = {};
result.columns.forEach((col, index) => {
obj[col] = row[index];
});
return obj;
});
const formattedResult = {
crew_id: id,
results: results
};
allResults.push(formattedResult);
}
catch (error) {
logger.error(`Error retrieving seafarer details for ${id}:`, error);
// Continue processing other crew IDs
allResults.push({
crew_id: id,
results: [],
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
}
}
finally {
await returnConnectionToPool(client);
}
}
catch (error) {
logger.error('Error in getSeafarerDetailsFromSnowflake:', error);
throw error;
}
logger.info(`Retrieved seafarer details for ${allResults.length} crew members`);
return allResults;
}
//# sourceMappingURL=snowflake.js.map