@turbot/tailpipe-mcp
Version:
Tailpipe MCP server to query cloud and security logs using AI.
193 lines • 7.22 kB
JavaScript
import { resolve } from "path";
import { existsSync } from "fs";
import { logger } from "./logger.js";
import duckdb from 'duckdb';
import { executeCommand } from "../utils/command.js";
import { buildTailpipeCommand, getTailpipeEnv } from "../utils/tailpipe.js";
export class DatabaseService {
db = null;
connection = null;
config;
isConnecting = false;
constructor(config) {
this.config = config;
}
get databasePath() {
return this.config.path;
}
get sourceType() {
return this.config.sourceType;
}
/**
* Create a new DatabaseService instance and initialize the connection
*/
static async create(providedPath) {
const pathInfo = await DatabaseService.resolveDatabasePath(providedPath);
logger.info(`Database path resolved to: ${pathInfo.path}`);
logger.info(`Database path source type: ${pathInfo.sourceType}`);
const service = new DatabaseService({
path: pathInfo.path,
sourceType: pathInfo.sourceType
});
await service.initialize();
await service.testConnection();
return service;
}
/**
* Get database path from various sources in order of precedence:
* 1. Provided path argument
* 2. Environment variable TAILPIPE_MCP_DATABASE_PATH
* 3. Command line argument
* 4. Tailpipe CLI
*/
static async resolveDatabasePath(providedPath) {
// Check explicit path argument
const pathToUse = providedPath || process.env.TAILPIPE_MCP_DATABASE_PATH || (process.argv.length > 2 ? process.argv[2] : undefined);
if (pathToUse) {
const source = providedPath ? 'provided argument' :
process.env.TAILPIPE_MCP_DATABASE_PATH ? 'environment variable' :
'command line argument';
logger.info(`Database path provided via ${source}: ${pathToUse}`);
const resolvedPath = resolve(pathToUse);
logger.info(`Resolved database path to: ${resolvedPath}`);
if (!existsSync(resolvedPath)) {
throw new Error(`Database file does not exist: ${resolvedPath}`);
}
return {
path: resolvedPath,
source,
sourceType: 'cli-arg'
};
}
// Fall back to Tailpipe CLI
try {
logger.info('No database path provided, attempting to use Tailpipe CLI...');
// Debug Tailpipe CLI environment if needed
logger.debug('PATH environment variable:', process.env.PATH);
logger.debug('Which tailpipe:', executeCommand('which tailpipe || echo "not found"', { env: getTailpipeEnv() }));
const cmd = buildTailpipeCommand('connect', { output: 'json' });
const output = executeCommand(cmd, { env: getTailpipeEnv() });
const result = JSON.parse(output);
if (!result?.database_filepath) {
logger.error('Tailpipe connect output JSON:', JSON.stringify(result));
throw new Error('Tailpipe connect output missing database_filepath field');
}
const path = resolve(result.database_filepath);
logger.info(`Using Tailpipe database path: ${path}`);
if (!existsSync(path)) {
throw new Error(`Tailpipe database file does not exist: ${path}`);
}
return {
path,
source: 'tailpipe CLI connection',
sourceType: 'tailpipe'
};
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('Failed to get database path from Tailpipe CLI:', message);
throw new Error('Failed to get database path. Please install Tailpipe CLI or provide a database path directly.');
}
}
/**
* Initialize the database connection
*/
async initialize() {
if (this.isConnecting) {
throw new Error('Database connection already in progress');
}
try {
this.isConnecting = true;
await this.connect();
}
finally {
this.isConnecting = false;
}
}
/**
* Test the database connection with a simple query
*/
async testConnection() {
try {
logger.info("Testing database connection...");
await this.executeQuery("SELECT 1 as test");
logger.info("Database connection verified successfully");
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error("Database connection test failed:", message);
throw error;
}
}
async setDatabaseConfig(newConfig) {
this.config = {
...this.config,
...newConfig
};
await this.initialize();
await this.testConnection();
}
async connect() {
// Clean up any existing connections
await this.close();
try {
logger.debug(`Connecting to database: ${this.config.path}`);
this.db = new duckdb.Database(this.config.path, { access_mode: 'READ_ONLY' });
this.connection = this.db.connect();
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error(`Failed to connect to database: ${message}`);
throw error;
}
}
async executeQuery(sql, params = []) {
if (!this.connection) {
await this.initialize();
}
return this.runQuery(sql, params);
}
async runQuery(sql, params) {
return new Promise((resolve, reject) => {
const queryFn = params.length > 0 ?
(callback) => this.connection.all(sql, params, callback) :
(callback) => this.connection.all(sql, callback);
queryFn((err, rows) => {
if (err) {
reject(err);
}
else {
resolve(rows || []);
}
});
});
}
async close() {
const errors = [];
try {
if (this.connection) {
logger.debug('Closing database connection');
this.connection.close();
this.connection = null;
}
}
catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
}
try {
if (this.db) {
logger.debug('Closing database');
await new Promise((resolve) => this.db.close(() => resolve()));
this.db = null;
}
}
catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
}
if (errors.length > 0) {
logger.error('Errors occurred while closing database:', errors);
throw new AggregateError(errors, 'Failed to close database cleanly');
}
}
}
//# sourceMappingURL=database.js.map