Skip to main content
Glama
srthkdev

DBeaver MCP Server

by srthkdev
dbeaver-client.ts15 kB
import { spawn, ChildProcess } from 'child_process'; import fs from 'fs'; import path from 'path'; import os from 'os'; import csv from 'csv-parser'; import { Client } from 'pg'; import { DBeaverConnection, QueryResult, SchemaInfo, ExportOptions, ConnectionTest, DatabaseStats } from './types.js'; import { findDBeaverExecutable, getTestQuery, parseVersionFromResult, buildSchemaQuery, buildListTablesQuery } from './utils.js'; export class DBeaverClient { private executablePath: string; private timeout: number; private debug: boolean; constructor(executablePath?: string, timeout: number = 30000, debug: boolean = false) { this.executablePath = executablePath || findDBeaverExecutable(); this.timeout = timeout; this.debug = debug; } async executeQuery(connection: DBeaverConnection, query: string): Promise<QueryResult> { const startTime = Date.now(); try { // Use native database tools instead of DBeaver command line const result = await this.executeWithNativeTool(connection, query); result.executionTime = Date.now() - startTime; return result; } catch (error) { throw new Error(`Query execution failed: ${error}`); } } async testConnection(connection: DBeaverConnection): Promise<ConnectionTest> { const startTime = Date.now(); try { // Simple test query based on database type const testQuery = this.getTestQuery(connection.driver); const result = await this.executeQuery(connection, testQuery); return { connectionId: connection.id, success: true, responseTime: Date.now() - startTime, databaseVersion: this.extractVersionFromResult(result) }; } catch (error) { return { connectionId: connection.id, success: false, error: error instanceof Error ? error.message : String(error), responseTime: Date.now() - startTime }; } } async getTableSchema(connection: DBeaverConnection, tableName: string): Promise<SchemaInfo> { const schemaQuery = this.buildSchemaQuery(connection.driver, tableName); const result = await this.executeQuery(connection, schemaQuery); return this.parseSchemaResult(result, tableName); } async exportData( connection: DBeaverConnection, query: string, options: ExportOptions ): Promise<string> { const tempDir = os.tmpdir(); const exportId = `export_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const sqlFile = path.join(tempDir, `${exportId}.sql`); const outputFile = path.join(tempDir, `${exportId}_output.${options.format || 'csv'}`); try { // Write query to temporary file fs.writeFileSync(sqlFile, query, 'utf-8'); // Build DBeaver command arguments const args = [ '-nosplash', '-reuseWorkspace', '-con', connection.id, '-f', sqlFile, '-o', outputFile, '-of', options.format || 'csv', '-quit' ]; await this.executeDBeaver(args); // Optionally, you could check if the file exists and return the path if (!fs.existsSync(outputFile)) { throw new Error('Export failed: output file not found'); } return outputFile; } catch (error) { throw new Error(`Export failed: ${error}`); } finally { // Cleanup the SQL file, but keep the output file for the user this.cleanupFiles([sqlFile]); } } private async executeWithNativeTool(connection: DBeaverConnection, query: string): Promise<QueryResult> { const driver = connection.driver.toLowerCase(); if (driver.includes('sqlite')) { return this.executeSQLiteQuery(connection, query); } else if (driver.includes('postgres')) { return this.executePostgreSQLQuery(connection, query); } else { // For unsupported drivers, return a helpful error instead of crashing throw new Error(`Database driver "${driver}" is not yet supported for direct query execution. Supported drivers: SQLite, PostgreSQL. Please use DBeaver GUI for this connection type.`); } } private async executeSQLiteQuery(connection: DBeaverConnection, query: string): Promise<QueryResult> { return new Promise((resolve, reject) => { const dbPath = connection.properties?.database || connection.database; if (!dbPath) { reject(new Error('SQLite database path not found')); return; } const proc = spawn('sqlite3', [dbPath, '-header', '-csv'], { stdio: ['pipe', 'pipe', 'pipe'] }); let output = ''; let error = ''; proc.stdout.on('data', (data) => { output += data.toString(); }); proc.stderr.on('data', (data) => { error += data.toString(); }); proc.on('close', (code) => { if (code !== 0) { reject(new Error(`SQLite error: ${error}`)); return; } const lines = output.trim().split('\n'); if (lines.length === 0) { resolve({ columns: [], rows: [], rowCount: 0, executionTime: 0 }); return; } const columns = lines[0].split(','); const rows = lines.slice(1).map(line => line.split(',')); resolve({ columns, rows, rowCount: rows.length, executionTime: 0 }); }); proc.stdin.write(query); proc.stdin.end(); }); } private async executePostgreSQLQuery(connection: DBeaverConnection, query: string): Promise<QueryResult> { const host = connection.host || connection.properties?.host || 'localhost'; const port = connection.port || (connection.properties?.port ? parseInt(connection.properties.port) : 5432); const database = connection.database || connection.properties?.database || 'postgres'; const user = connection.user || connection.properties?.user || process.env.PGUSER || 'postgres'; const password = connection.properties?.password || process.env.PGPASSWORD; // SSL handling const sslModeRaw = connection.properties?.['ssl.mode'] || connection.properties?.['sslmode'] || connection.properties?.['ssl']; const sslMode = String(sslModeRaw ?? '').toLowerCase(); const sslRootCert = connection.properties?.['sslrootcert'] || connection.properties?.['ssl.root.cert'] || connection.properties?.['sslRootCert']; const sslCert = connection.properties?.['sslcert'] || connection.properties?.['ssl.cert'] || connection.properties?.['sslCert']; const sslKey = connection.properties?.['sslkey'] || connection.properties?.['ssl.key'] || connection.properties?.['sslKey']; let ssl: any = undefined; const requireSsl = ['require', 'verify-ca', 'verify-full', 'true', '1'].includes(sslMode); const verifyModes = ['verify-ca', 'verify-full']; const disableSsl = ['disable', 'false', '0'].includes(sslMode); if (requireSsl) { const sslObj: any = {}; try { if (sslRootCert && fs.existsSync(String(sslRootCert))) sslObj.ca = fs.readFileSync(String(sslRootCert)).toString(); if (sslCert && fs.existsSync(String(sslCert))) sslObj.cert = fs.readFileSync(String(sslCert)).toString(); if (sslKey && fs.existsSync(String(sslKey))) sslObj.key = fs.readFileSync(String(sslKey)).toString(); } catch (e) { // ignore errors, we'll set a reasonable default below } const hasCa = typeof sslObj.ca === 'string' && sslObj.ca.length > 0; if (verifyModes.includes(sslMode)) { // Enforce certificate verification. If no custom CA is provided, fallback to system trust store. sslObj.rejectUnauthorized = true; if (this.debug && !hasCa) { // eslint-disable-next-line no-console console.warn('sslMode set to verify-ca/verify-full but no sslrootcert provided; using system CA store'); } } else { // "require" mode: encrypt without verification sslObj.rejectUnauthorized = false; } ssl = sslObj; } else if (disableSsl) { ssl = false; } const client = new Client({ host, port, database, user, password, ssl }); try { await client.connect(); const res = await client.query(query); const columns: string[] = (res.fields || []).map((f: any) => f.name as string); const rows: any[][] = (res.rows || []).map((r: any) => columns.map((c: string) => r[c])); return { columns, rows, rowCount: typeof res.rowCount === 'number' ? res.rowCount : rows.length, executionTime: 0 }; } finally { try { await client.end(); } catch (closeError) { // ALWAYS log connection cleanup failures - they indicate resource leaks console.error('Failed to close PostgreSQL connection:', { error: closeError instanceof Error ? closeError.message : String(closeError), host, database }); } } } private async executeDBeaver(args: string[]): Promise<void> { return new Promise((resolve, reject) => { const proc = spawn(this.executablePath, args, { stdio: this.debug ? 'inherit' : 'ignore' }); // Set up timeout const timeoutId = setTimeout(() => { proc.kill('SIGTERM'); reject(new Error(`DBeaver execution timed out after ${this.timeout}ms`)); }, this.timeout); proc.on('error', (error) => { clearTimeout(timeoutId); reject(error); }); proc.on('exit', (code) => { clearTimeout(timeoutId); if (code === 0) { resolve(); } else { reject(new Error(`DBeaver exited with code ${code}`)); } }); }); } private cleanupFiles(files: string[]): void { for (const file of files) { try { if (fs.existsSync(file)) { fs.unlinkSync(file); } } catch (e) { // Ignore errors } } } private async parseCSVOutput(filePath: string): Promise<QueryResult> { return new Promise((resolve, reject) => { const rows: any[] = []; let columns: string[] = []; let rowCount = 0; // Check if file exists first if (!fs.existsSync(filePath)) { reject(new Error(`Output file not found: ${filePath}`)); return; } fs.createReadStream(filePath) .pipe(csv()) .on('headers', (headers) => { columns = headers; }) .on('data', (data) => { rows.push(Object.values(data)); rowCount++; }) .on('end', () => { resolve({ columns, rows, rowCount, executionTime: 0 }); }) .on('error', (error) => { reject(new Error(`CSV parsing failed: ${error.message}`)); }); }); } private getTestQuery(driver: string): string { // Delegate to utils return getTestQuery(driver); } private extractVersionFromResult(result: any): string | undefined { return parseVersionFromResult(result); } private buildSchemaQuery(driver: string, tableName: string): string { return buildSchemaQuery(driver, tableName); } private parseSchemaResult(result: any, tableName: string): SchemaInfo { const columns: any[] = []; if (result.rows && result.columns) { // Parse each row as a column definition result.rows.forEach((row: any[]) => { const columnInfo: any = { name: '', type: 'string', nullable: true, isPrimaryKey: false, isAutoIncrement: false }; // Map columns based on the query result structure result.columns.forEach((colName: string, idx: number) => { const value = row[idx]; switch (colName.toLowerCase()) { case 'column_name': case 'name': columnInfo.name = value || ''; break; case 'data_type': case 'type': columnInfo.type = value || 'string'; break; case 'is_nullable': case 'nullable': columnInfo.nullable = value === 'YES' || value === 'Y' || value === true; break; case 'column_default': case 'default': columnInfo.defaultValue = value; break; case 'column_key': case 'key': columnInfo.isPrimaryKey = value === 'PRI' || value === 'PRIMARY'; break; case 'extra': columnInfo.isAutoIncrement = value && value.toLowerCase().includes('auto_increment'); break; case 'character_maximum_length': case 'length': columnInfo.length = parseInt(value) || undefined; break; case 'numeric_precision': case 'precision': columnInfo.precision = parseInt(value) || undefined; break; case 'numeric_scale': case 'scale': columnInfo.scale = parseInt(value) || undefined; break; } }); if (columnInfo.name) { columns.push(columnInfo); } }); } return { tableName, columns, indexes: [], constraints: [] }; } async getDatabaseStats(connection: DBeaverConnection): Promise<DatabaseStats> { const startTime = Date.now(); try { // Get table count const tables = await this.listTables(connection, undefined, true); const tableCount = tables.length; // Get server version const versionQuery = this.getTestQuery(connection.driver); const versionResult = await this.executeQuery(connection, versionQuery); const serverVersion = this.extractVersionFromResult(versionResult) || 'Unknown'; return { connectionId: connection.id, tableCount, totalSize: 'Unknown', // Would need specific queries per database type connectionTime: Date.now() - startTime, serverVersion }; } catch (error) { return { connectionId: connection.id, tableCount: 0, totalSize: 'Unknown', connectionTime: Date.now() - startTime, serverVersion: 'Unknown' }; } } async listTables(connection: DBeaverConnection, schema?: string, includeViews: boolean = false): Promise<any[]> { try { const query = buildListTablesQuery(connection.driver, schema, includeViews); const result = await this.executeQuery(connection, query); // Convert result to table objects return result.rows.map(row => { const tableObj: any = {}; result.columns.forEach((col, idx) => { tableObj[col] = row[idx]; }); return tableObj; }); } catch (error) { if (this.debug) { console.error(`Failed to list tables: ${error}`); } // Return empty array instead of crashing return []; } } }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/srthkdev/dbeaver-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server