Skip to main content
Glama

DBeaver MCP Server

by srthkdev
config-parser.ts14 kB
import fs from 'fs'; import path from 'path'; import os from 'os'; import { parseString } from 'xml2js'; import { promisify } from 'util'; import crypto from 'crypto'; import { DBeaverConnection, DBeaverConfig } from './types.js'; const parseXML = promisify(parseString); // DBeaver uses these hardcoded values for password encryption const DBEAVER_AES_KEY = Buffer.from('babb4a9f774ab853c96c2d653dfe544a', 'hex'); const DBEAVER_AES_IV = Buffer.alloc(16, 0); export class DBeaverConfigParser { private config: DBeaverConfig; private isNewFormat: boolean = false; constructor(config: DBeaverConfig = {}) { this.config = { workspacePath: config.workspacePath || this.getDefaultWorkspacePath(), debug: config.debug || false, ...config }; // Detect if we're using the new DBeaver format this.isNewFormat = this.detectNewFormat(); } private detectNewFormat(): boolean { const newFormatPath = path.join( this.config.workspacePath!, 'General', '.dbeaver', 'data-sources.json' ); const oldFormatPath = path.join( this.config.workspacePath!, '.metadata', '.plugins', 'org.jkiss.dbeaver.core', 'connections.xml' ); // If new format exists, use it if (fs.existsSync(newFormatPath)) { return true; } // If old format exists, use it if (fs.existsSync(oldFormatPath)) { return false; } // If neither exists, check for new format directory structure const newFormatDir = path.join(this.config.workspacePath!, 'General', '.dbeaver'); const oldFormatDir = path.join(this.config.workspacePath!, '.metadata'); // Prefer new format if its directory structure exists if (fs.existsSync(newFormatDir)) { return true; } // Default to old format if metadata directory exists if (fs.existsSync(oldFormatDir)) { return false; } // Default to new format for newer DBeaver installations return true; } private getDefaultWorkspacePath(): string { const platform = os.platform(); const homeDir = os.homedir(); switch (platform) { case 'win32': return path.join(homeDir, 'AppData', 'Roaming', 'DBeaverData', 'workspace6'); case 'darwin': return path.join(homeDir, 'Library', 'DBeaverData', 'workspace6'); default: // Linux and others return path.join(homeDir, '.local', 'share', 'DBeaverData', 'workspace6'); } } private getConnectionsFilePath(): string { if (this.isNewFormat) { return path.join( this.config.workspacePath!, 'General', '.dbeaver', 'data-sources.json' ); } else { return path.join( this.config.workspacePath!, '.metadata', '.plugins', 'org.jkiss.dbeaver.core', 'connections.xml' ); } } private getCredentialsFilePath(): string { if (this.isNewFormat) { return path.join( this.config.workspacePath!, 'General', '.dbeaver', 'credentials-config.json' ); } else { return path.join( this.config.workspacePath!, '.metadata', '.plugins', 'org.jkiss.dbeaver.core', 'credentials-config.json' ); } } async parseConnections(): Promise<DBeaverConnection[]> { const connectionsFile = this.getConnectionsFilePath(); if (!fs.existsSync(connectionsFile)) { // Try the alternative format if the detected format file doesn't exist const alternativeFormat = !this.isNewFormat; const alternativeFile = alternativeFormat ? path.join(this.config.workspacePath!, 'General', '.dbeaver', 'data-sources.json') : path.join(this.config.workspacePath!, '.metadata', '.plugins', 'org.jkiss.dbeaver.core', 'connections.xml'); if (fs.existsSync(alternativeFile)) { // Switch to the alternative format and retry this.isNewFormat = alternativeFormat; return this.parseConnections(); } // Neither format exists - return empty array instead of throwing error if (this.config.debug) { console.warn(`No DBeaver connections found. Checked:\n- ${connectionsFile}\n- ${alternativeFile}`); } return []; } try { let connections: DBeaverConnection[] = []; if (this.isNewFormat) { connections = await this.parseNewFormatConnections(connectionsFile); } else { connections = await this.parseOldFormatConnections(connectionsFile); } // Load and merge credentials await this.loadCredentials(connections); return connections; } catch (error) { throw new Error(`Failed to parse DBeaver connections: ${error}`); } } private async parseNewFormatConnections(filePath: string): Promise<DBeaverConnection[]> { const jsonContent = fs.readFileSync(filePath, 'utf-8'); const data = JSON.parse(jsonContent); const connections: DBeaverConnection[] = []; if (!data.connections) { return connections; } for (const [connectionId, connData] of Object.entries(data.connections)) { const conn = connData as any; const connection: DBeaverConnection = { id: connectionId, name: conn.name || connectionId, driver: conn.driver || conn.provider || '', url: '', folder: conn.folder || '', description: conn.description || '', readonly: conn.readonly === true }; // Extract properties from the new format if (conn.configuration) { const config = conn.configuration; connection.properties = { url: config.url || '', user: config.user || '', host: config.host || '', port: config.port ? String(config.port) : '', database: config.database || '', server: config.server || '', ...config }; connection.url = config.url || ''; connection.user = config.user || ''; connection.host = config.host || config.server || ''; connection.port = config.port ? parseInt(String(config.port)) : undefined; connection.database = config.database || ''; } connections.push(connection); } return connections; } private async parseOldFormatConnections(filePath: string): Promise<DBeaverConnection[]> { const xmlContent = fs.readFileSync(filePath, 'utf-8'); const result = await parseXML(xmlContent); return this.extractConnections(result); } private extractConnections(xmlData: any): DBeaverConnection[] { const connections: DBeaverConnection[] = []; if (!xmlData.connections || !xmlData.connections.connection) { return connections; } const connectionArray = Array.isArray(xmlData.connections.connection) ? xmlData.connections.connection : [xmlData.connections.connection]; for (const conn of connectionArray) { const connection: DBeaverConnection = { id: conn.$.id || '', name: conn.$.name || '', driver: conn.$.driver || '', url: '', folder: conn.$.folder || '', description: conn.$.description || '', readonly: conn.$.readonly === 'true' }; // Extract properties if (conn.property) { const properties: Record<string, string> = {}; const propArray = Array.isArray(conn.property) ? conn.property : [conn.property]; for (const prop of propArray) { if (prop.$ && prop.$.name && prop.$.value) { properties[prop.$.name] = prop.$.value; } } connection.properties = properties; connection.url = properties.url || ''; connection.user = properties.user || ''; connection.host = properties.host || ''; connection.port = properties.port ? parseInt(properties.port) : undefined; connection.database = properties.database || ''; } connections.push(connection); } return connections; } async getConnection(connectionId: string): Promise<DBeaverConnection | null> { try { const connections = await this.parseConnections(); return connections.find(conn => conn.id === connectionId || conn.name === connectionId ) || null; } catch (error) { if (this.config.debug) { console.error(`Failed to get connection ${connectionId}: ${error}`); } return null; } } async validateConnection(connectionId: string): Promise<boolean> { const connection = await this.getConnection(connectionId); if (!connection) { return false; } // Basic validation - check if essential properties exist return !!(connection.url || (connection.host && connection.driver)); } getWorkspacePath(): string { return this.config.workspacePath!; } async getDriverInfo(driverId: string): Promise<any> { if (this.isNewFormat) { // New format doesn't have a separate drivers.xml file // Driver info is embedded in the data-sources.json return null; } const driversFile = path.join( this.config.workspacePath!, '.metadata', '.plugins', 'org.jkiss.dbeaver.core', 'drivers.xml' ); if (!fs.existsSync(driversFile)) { return null; } try { const xmlContent = fs.readFileSync(driversFile, 'utf-8'); const result: any = await parseXML(xmlContent); if (!result.drivers || !result.drivers.driver) { return null; } const driverArray = Array.isArray(result.drivers.driver) ? result.drivers.driver : [result.drivers.driver]; return driverArray.find((driver: any) => driver.$.id === driverId) || null; } catch (error) { if (this.config.debug) { console.error(`Failed to parse drivers file: ${error}`); } return null; } } async getConnectionFolders(): Promise<string[]> { const connections = await this.parseConnections(); const folders = new Set<string>(); connections.forEach(conn => { if (conn.folder) { folders.add(conn.folder); } }); return Array.from(folders).sort(); } isWorkspaceValid(): boolean { const workspacePath = this.config.workspacePath!; if (this.isNewFormat) { const newFormatPath = path.join(workspacePath, 'General', '.dbeaver'); return fs.existsSync(workspacePath) && fs.existsSync(newFormatPath); } else { const metadataPath = path.join(workspacePath, '.metadata'); return fs.existsSync(workspacePath) && fs.existsSync(metadataPath); } } getDebugInfo(): object { return { workspacePath: this.config.workspacePath, connectionsFile: this.getConnectionsFilePath(), connectionsFileExists: fs.existsSync(this.getConnectionsFilePath()), credentialsFile: this.getCredentialsFilePath(), credentialsFileExists: fs.existsSync(this.getCredentialsFilePath()), workspaceValid: this.isWorkspaceValid(), isNewFormat: this.isNewFormat, platform: os.platform(), nodeVersion: process.version }; } /** * Load and decrypt credentials from DBeaver's credentials-config.json */ private async loadCredentials(connections: DBeaverConnection[]): Promise<void> { const credentialsFile = this.getCredentialsFilePath(); if (!fs.existsSync(credentialsFile)) { if (this.config.debug) { console.warn(`Credentials file not found: ${credentialsFile}`); } return; } try { const encryptedData = fs.readFileSync(credentialsFile); const decryptedData = this.decryptCredentials(encryptedData); const credentials = JSON.parse(decryptedData); // Merge credentials into connections for (const connection of connections) { const connId = connection.id; // Look for credentials in the decrypted data if (credentials[connId]) { const connCreds = credentials[connId]; // Extract credentials from the nested structure if (connCreds['#connection']) { const creds = connCreds['#connection']; if (creds.user) { connection.user = creds.user; if (!connection.properties) { connection.properties = {}; } connection.properties.user = creds.user; } if (creds.password) { if (!connection.properties) { connection.properties = {}; } connection.properties.password = creds.password; } } } } if (this.config.debug) { console.error(`Successfully loaded credentials for ${connections.length} connections`); } } catch (error) { if (this.config.debug) { console.error(`Failed to load credentials: ${error}`); } // Don't throw - continue without credentials } } /** * Decrypt DBeaver credentials using AES-128-CBC * DBeaver uses a hardcoded key and IV for encryption */ private decryptCredentials(encryptedData: Buffer): string { try { // Create decipher with DBeaver's hardcoded key and IV const decipher = crypto.createDecipheriv('aes-128-cbc', DBEAVER_AES_KEY, DBEAVER_AES_IV); decipher.setAutoPadding(true); // Decrypt entire file, then drop the 16-byte header from the decrypted output let decrypted = decipher.update(encryptedData); decrypted = Buffer.concat([decrypted, decipher.final()]); const withoutHeader = decrypted.slice(16); return withoutHeader.toString('utf8'); } catch (error) { throw new Error(`Failed to decrypt credentials: ${error}`); } } }

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