@hechtcarmel/vertica-mcp
Version:
MCP (Model Context Protocol) server for readonly Vertica database operations
270 lines • 9.55 kB
JavaScript
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