UNPKG

@hechtcarmel/vertica-mcp

Version:

MCP (Model Context Protocol) server for readonly Vertica database operations

270 lines 9.55 kB
import vertica from "vertica-nodejs"; import { READONLY_QUERY_PREFIXES, LOG_MESSAGES } from "../constants/index.js"; import { determineTableType, resolveSchemaName, } from "../utils/table-helpers.js"; const verticaTyped = vertica; export class VerticaService { config; connection = null; readonlyQueryPrefixes = READONLY_QUERY_PREFIXES; constructor(config) { this.config = config; } async connect() { if (this.connection) { return; } try { const clientConfig = { host: this.config.host, port: this.config.port, database: this.config.database, user: this.config.user, password: this.config.password, connectionTimeoutMillis: this.config.queryTimeout || 30000, }; if (this.config.ssl) { clientConfig.ssl = true; if (this.config.sslRejectUnauthorized !== undefined) { clientConfig.ssl = this.config.sslRejectUnauthorized; } } else { clientConfig.ssl = false; } const client = new verticaTyped.Client(clientConfig); await client.connect(); this.connection = client; console.log(`${LOG_MESSAGES.DB_CONNECTED}: ${this.config.host}:${this.config.port}/${this.config.database}`); } catch (error) { throw new Error(`Failed to connect to Vertica: ${error instanceof Error ? error.message : String(error)}`); } } async disconnect() { if (this.connection) { try { await this.connection.end(); console.log(LOG_MESSAGES.DB_DISCONNECTED); } catch (error) { console.warn(LOG_MESSAGES.DB_CONNECTION_WARNING, error instanceof Error ? error.message : String(error)); } finally { this.connection = null; } } } isConnected() { return this.connection !== null; } validateReadonlyQuery(sql) { const trimmedSql = sql.trim().toUpperCase(); const isReadonly = this.readonlyQueryPrefixes.some((prefix) => trimmedSql.startsWith(prefix)); if (!isReadonly) { throw new Error(`Only readonly queries are allowed. Query must start with: ${this.readonlyQueryPrefixes.join(", ")}`); } } async executeQuery(sql, params) { this.validateReadonlyQuery(sql); await this.connect(); if (!this.connection) { throw new Error("Database connection not established"); } try { const result = await this.connection.query(sql, params); return { rows: result.rows || [], rowCount: result.rowCount || 0, fields: result.fields || [], command: result.command || "", }; } catch (error) { throw new Error(`Query execution failed: ${error instanceof Error ? error.message : String(error)}`); } } async *streamQuery(sql, options) { this.validateReadonlyQuery(sql); await this.connect(); if (!this.connection) { throw new Error("Database connection not established"); } const { batchSize, maxRows } = options; let offset = 0; let batchNumber = 0; let totalFetched = 0; try { while (true) { const limitedSql = `${sql} LIMIT ${batchSize} OFFSET ${offset}`; const result = await this.connection.query(limitedSql); if (!result.rows || result.rows.length === 0) { break; } batchNumber++; totalFetched += result.rows.length; const hasMore = result.rows.length === batchSize && (!maxRows || totalFetched < maxRows); yield { batch: result.rows, batchNumber, totalBatches: hasMore ? batchNumber + 1 : batchNumber, hasMore, fields: result.fields || [], }; if (!hasMore || (maxRows && totalFetched >= maxRows)) { break; } offset += batchSize; } } catch (error) { throw new Error(`Stream query failed: ${error instanceof Error ? error.message : String(error)}`); } } async getTableStructure(tableName, schemaName) { const schema = resolveSchemaName(schemaName, this.config.defaultSchema); const columnsQuery = ` SELECT column_name, data_type, is_nullable, column_default, character_maximum_length, numeric_precision, numeric_scale, ordinal_position FROM v_catalog.columns WHERE table_schema = ? AND table_name = ? ORDER BY ordinal_position `; const columnsResult = await this.executeQuery(columnsQuery, [ schema, tableName, ]); const columns = columnsResult.rows.map((row) => ({ columnName: row.column_name, dataType: row.data_type, isNullable: row.is_nullable === "YES", defaultValue: row.column_default, columnSize: row.character_maximum_length, decimalDigits: row.numeric_scale, ordinalPosition: row.ordinal_position, })); const tableQuery = ` SELECT owner_name, is_temp_table, is_system_table, is_flextable FROM v_catalog.tables WHERE table_schema = ? AND table_name = ? `; const tableResult = await this.executeQuery(tableQuery, [ schema, tableName, ]); if (tableResult.rows.length === 0) { throw new Error(`Table ${schema}.${tableName} not found`); } const tableInfo = tableResult.rows[0]; const tableType = determineTableType({ is_temp_table: tableInfo.is_temp_table, is_system_table: tableInfo.is_system_table, is_flextable: tableInfo.is_flextable, }); const constraints = []; return { schemaName: schema, tableName, columns, constraints, tableType, owner: tableInfo.owner_name, }; } async listTables(schemaName) { const schema = resolveSchemaName(schemaName, this.config.defaultSchema); const query = ` SELECT table_schema, table_name, owner_name, is_temp_table, is_system_table, is_flextable FROM v_catalog.tables WHERE table_schema = ? ORDER BY table_name `; const result = await this.executeQuery(query, [schema]); return result.rows.map((row) => { const tableType = determineTableType({ is_temp_table: row.is_temp_table, is_system_table: row.is_system_table, is_flextable: row.is_flextable, }); return { schemaName: row.table_schema, tableName: row.table_name, tableType, owner: row.owner_name, }; }); } async listViews(schemaName) { const schema = resolveSchemaName(schemaName, this.config.defaultSchema); const query = ` SELECT table_schema, table_name as view_name, view_definition, owner_name FROM v_catalog.views WHERE table_schema = ? ORDER BY table_name `; const result = await this.executeQuery(query, [schema]); return result.rows.map((row) => ({ schemaName: row.table_schema, viewName: row.view_name, definition: row.view_definition, owner: row.owner_name, })); } async listIndexes(tableName, schemaName) { const schema = resolveSchemaName(schemaName, this.config.defaultSchema); const query = ` SELECT p.projection_name as index_name, p.anchor_table_name as table_name, pc.projection_column_name as column_name, p.is_key_constraint_projection, pc.sort_position FROM v_catalog.projection_columns pc JOIN v_catalog.projections p ON pc.projection_id = p.projection_id WHERE p.projection_schema = ? AND p.anchor_table_name = ? ORDER BY p.projection_name, pc.sort_position `; const result = await this.executeQuery(query, [schema, tableName]); return result.rows.map((row) => ({ indexName: row.index_name, tableName: row.table_name, columnName: row.column_name, isUnique: row.is_key_constraint_projection === "t" || row.is_key_constraint_projection === true, indexType: "projection", ordinalPosition: row.sort_position, })); } async testConnection() { try { await this.connect(); const result = await this.executeQuery("SELECT 1 as test"); return result.rows.length > 0 && result.rows[0]?.test === 1; } catch { return false; } } } //# sourceMappingURL=vertica-service.js.map