Skip to main content
Glama

DBeaver MCP Server

by srthkdev
index.ts33.6 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import { DBeaverConfigParser } from './config-parser.js'; import { DBeaverClient } from './dbeaver-client.js'; import { DBeaverConnection, QueryResult, ExportOptions, BusinessInsight, TableResource } from './types.js'; import { validateQuery, sanitizeConnectionId, formatError, convertToCSV } from './utils.js'; // CSV functionality will be handled in utils import fs from 'fs'; import os from 'os'; import path from 'path'; import { fileURLToPath } from 'url'; // Get version from package.json const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8')); const VERSION = packageJson.version; class DBeaverMCPServer { private server: Server; private configParser: DBeaverConfigParser; private dbeaverClient: DBeaverClient; private debug: boolean; private insights: BusinessInsight[] = []; private insightsFile: string; constructor() { this.debug = process.env.DBEAVER_DEBUG === 'true'; this.insightsFile = path.join(os.tmpdir(), 'dbeaver-mcp-insights.json'); this.server = new Server( { name: 'dbeaver-mcp-server', version: VERSION, }, { capabilities: { tools: {}, resources: {}, }, } ); this.configParser = new DBeaverConfigParser({ debug: this.debug, timeout: parseInt(process.env.DBEAVER_TIMEOUT || '30000'), executablePath: process.env.DBEAVER_PATH }); this.dbeaverClient = new DBeaverClient( process.env.DBEAVER_PATH, parseInt(process.env.DBEAVER_TIMEOUT || '30000'), this.debug ); this.loadInsights(); this.setupResourceHandlers(); this.setupToolHandlers(); this.setupErrorHandling(); } private log(message: string, level: 'info' | 'error' | 'debug' = 'info') { if (level === 'debug' && !this.debug) return; const timestamp = new Date().toISOString(); const prefix = `[${timestamp}] [${level.toUpperCase()}]`; if (level === 'error') { console.error(`${prefix} ${message}`); } else { console.error(`${prefix} ${message}`); } } private loadInsights() { try { if (fs.existsSync(this.insightsFile)) { const data = fs.readFileSync(this.insightsFile, 'utf-8'); this.insights = JSON.parse(data); } } catch (error) { this.log(`Failed to load insights: ${error}`, 'debug'); this.insights = []; } } private saveInsights() { try { fs.writeFileSync(this.insightsFile, JSON.stringify(this.insights, null, 2)); } catch (error) { this.log(`Failed to save insights: ${error}`, 'error'); } } private setupErrorHandling() { process.on('uncaughtException', (error) => { this.log(`Uncaught exception: ${error.message}`, 'error'); if (this.debug) { this.log(error.stack || '', 'debug'); } process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { this.log(`Unhandled rejection at: ${promise}, reason: ${reason}`, 'error'); if (this.debug) { this.log(String(reason), 'debug'); } }); } private setupResourceHandlers() { // List all available database resources (table schemas) this.server.setRequestHandler(ListResourcesRequestSchema, async () => { try { const connections = await this.configParser.parseConnections(); const resources: any[] = []; for (const connection of connections) { try { const tables = await this.dbeaverClient.listTables(connection, undefined, false); for (const table of tables) { const tableName = typeof table === 'string' ? table : table.name || table.table_name; if (tableName) { resources.push({ uri: `dbeaver://${connection.id}/${tableName}/schema`, mimeType: "application/json", name: `"${tableName}" schema (${connection.name})`, description: `Schema information for table ${tableName} in ${connection.name}`, }); } } } catch (error) { this.log(`Failed to list tables for connection ${connection.name}: ${error}`, 'debug'); } } return { resources }; } catch (error) { this.log(`Failed to list resources: ${error}`, 'error'); return { resources: [] }; } }); // Get schema information for a specific table this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { try { const uri = new URL(request.params.uri); const pathParts = uri.pathname.split('/').filter(p => p); if (pathParts.length < 2 || pathParts[pathParts.length - 1] !== 'schema') { throw new Error("Invalid resource URI format"); } const connectionId = uri.hostname; const tableName = pathParts[pathParts.length - 2]; const connection = await this.configParser.getConnection(connectionId); if (!connection) { throw new Error(`Connection not found: ${connectionId}`); } const schema = await this.dbeaverClient.getTableSchema(connection, tableName); return { contents: [ { uri: request.params.uri, mimeType: "application/json", text: JSON.stringify(schema, null, 2), }, ], }; } catch (error) { throw new McpError(ErrorCode.InvalidParams, `Failed to read resource: ${formatError(error)}`); } }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { const tools: Tool[] = [ { name: 'list_connections', description: 'List all available DBeaver database connections', inputSchema: { type: 'object', properties: { includeDetails: { type: 'boolean', description: 'Include detailed connection information', default: false } } }, }, { name: 'get_connection_info', description: 'Get detailed information about a specific DBeaver connection', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'The ID or name of the DBeaver connection', }, }, required: ['connectionId'], }, }, { name: 'execute_query', description: 'Execute a SQL query on a specific DBeaver connection (read-only queries)', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'The ID or name of the DBeaver connection to use', }, query: { type: 'string', description: 'The SQL query to execute (SELECT statements only)', }, maxRows: { type: 'number', description: 'Maximum number of rows to return (default: 1000)', default: 1000 } }, required: ['connectionId', 'query'], }, }, { name: 'write_query', description: 'Execute INSERT, UPDATE, or DELETE queries on a specific DBeaver connection', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'The ID or name of the DBeaver connection to use', }, query: { type: 'string', description: 'The SQL query to execute (INSERT, UPDATE, DELETE)', }, }, required: ['connectionId', 'query'], }, }, { name: 'create_table', description: 'Create new tables in the database', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'The ID or name of the DBeaver connection', }, query: { type: 'string', description: 'CREATE TABLE statement', }, }, required: ['connectionId', 'query'], }, }, { name: 'alter_table', description: 'Modify existing table schema (add columns, rename tables, etc.)', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'The ID or name of the DBeaver connection', }, query: { type: 'string', description: 'ALTER TABLE statement', }, }, required: ['connectionId', 'query'], }, }, { name: 'drop_table', description: 'Remove a table from the database with safety confirmation', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'The ID or name of the DBeaver connection', }, tableName: { type: 'string', description: 'Name of the table to drop', }, confirm: { type: 'boolean', description: 'Safety confirmation flag (must be true)', }, }, required: ['connectionId', 'tableName', 'confirm'], }, }, { name: 'get_table_schema', description: 'Get schema information for a specific table', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'The ID or name of the DBeaver connection', }, tableName: { type: 'string', description: 'The name of the table to describe', }, includeIndexes: { type: 'boolean', description: 'Include index information', default: true } }, required: ['connectionId', 'tableName'], }, }, { name: 'export_data', description: 'Export query results to various formats (CSV, JSON, etc.)', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'The ID or name of the DBeaver connection', }, query: { type: 'string', description: 'The SQL query to execute for export (SELECT only)', }, format: { type: 'string', enum: ['csv', 'json', 'xml', 'excel'], description: 'Export format', default: 'csv' }, includeHeaders: { type: 'boolean', description: 'Include column headers in export', default: true }, maxRows: { type: 'number', description: 'Maximum number of rows to export', default: 10000 } }, required: ['connectionId', 'query'], }, }, { name: 'test_connection', description: 'Test connectivity to a DBeaver connection', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'The ID or name of the DBeaver connection to test', }, }, required: ['connectionId'], }, }, { name: 'get_database_stats', description: 'Get statistics and information about a database', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'The ID or name of the DBeaver connection', }, }, required: ['connectionId'], }, }, { name: 'list_tables', description: 'List all tables in a database', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'The ID or name of the DBeaver connection', }, schema: { type: 'string', description: 'Specific schema to list tables from (optional)', }, includeViews: { type: 'boolean', description: 'Include views in the results', default: false } }, required: ['connectionId'], }, }, { name: 'append_insight', description: 'Add a business insight or analysis note to the memo', inputSchema: { type: 'object', properties: { insight: { type: 'string', description: 'The business insight or analysis note to store', }, connection: { type: 'string', description: 'Optional connection ID to associate with this insight', }, tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags to categorize the insight', }, }, required: ['insight'], }, }, { name: 'list_insights', description: 'List all stored business insights and analysis notes', inputSchema: { type: 'object', properties: { connection: { type: 'string', description: 'Filter insights by connection ID (optional)', }, tags: { type: 'array', items: { type: 'string' }, description: 'Filter insights by tags (optional)', }, }, }, }, ]; return { tools }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { this.log(`Executing tool: ${name} with args: ${JSON.stringify(args)}`, 'debug'); switch (name) { case 'list_connections': return await this.handleListConnections(args as { includeDetails?: boolean }); case 'get_connection_info': return await this.handleGetConnectionInfo(args as { connectionId: string }); case 'execute_query': return await this.handleExecuteQuery(args as { connectionId: string; query: string; maxRows?: number }); case 'write_query': return await this.handleWriteQuery(args as { connectionId: string; query: string; }); case 'create_table': return await this.handleCreateTable(args as { connectionId: string; query: string; }); case 'alter_table': return await this.handleAlterTable(args as { connectionId: string; query: string; }); case 'drop_table': return await this.handleDropTable(args as { connectionId: string; tableName: string; confirm: boolean }); case 'get_table_schema': return await this.handleGetTableSchema(args as { connectionId: string; tableName: string; includeIndexes?: boolean }); case 'export_data': return await this.handleExportData(args as { connectionId: string; query: string; format?: string; includeHeaders?: boolean; maxRows?: number }); case 'test_connection': return await this.handleTestConnection(args as { connectionId: string }); case 'get_database_stats': return await this.handleGetDatabaseStats(args as { connectionId: string }); case 'list_tables': return await this.handleListTables(args as { connectionId: string; schema?: string; includeViews?: boolean }); case 'append_insight': return await this.handleAppendInsight(args as { insight: string; connection?: string; tags?: string[] }); case 'list_insights': return await this.handleListInsights(args as { connection?: string; tags?: string[] }); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error: any) { this.log(`Tool execution failed: ${error}`, 'error'); if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Tool execution failed: ${formatError(error)}` ); } }); } private async handleListConnections(args: { includeDetails?: boolean }) { const connections = await this.configParser.parseConnections(); if (args.includeDetails) { return { content: [{ type: 'text' as const, text: JSON.stringify(connections, null, 2), }], }; } const simplified = connections.map(conn => ({ id: conn.id, name: conn.name, driver: conn.driver, host: conn.host, database: conn.database, folder: conn.folder })); return { content: [{ type: 'text' as const, text: JSON.stringify(simplified, null, 2), }], }; } private async handleGetConnectionInfo(args: { connectionId: string }) { const connectionId = sanitizeConnectionId(args.connectionId); const connection = await this.configParser.getConnection(connectionId); if (!connection) { throw new McpError(ErrorCode.InvalidParams, `Connection not found: ${connectionId}`); } return { content: [{ type: 'text' as const, text: JSON.stringify(connection, null, 2), }], }; } private async handleExecuteQuery(args: { connectionId: string; query: string; maxRows?: number }) { const connectionId = sanitizeConnectionId(args.connectionId); const query = args.query.trim(); const maxRows = args.maxRows || 1000; // Validate query const validationError = validateQuery(query); if (validationError) { throw new McpError(ErrorCode.InvalidParams, validationError); } const connection = await this.configParser.getConnection(connectionId); if (!connection) { throw new McpError(ErrorCode.InvalidParams, `Connection not found: ${connectionId}`); } // Add LIMIT clause if not present and it's a SELECT query let finalQuery = query; if (query.toLowerCase().trimStart().startsWith('select') && !query.toLowerCase().includes('limit')) { finalQuery = `${query} LIMIT ${maxRows}`; } const result = await this.dbeaverClient.executeQuery(connection, finalQuery); const response = { query: finalQuery, connection: connection.name, executionTime: result.executionTime, rowCount: result.rowCount, columns: result.columns, rows: result.rows, truncated: result.rows.length >= maxRows }; return { content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2), }], }; } private async handleWriteQuery(args: { connectionId: string; query: string }) { const connectionId = sanitizeConnectionId(args.connectionId); const query = args.query.trim(); // Validate query type const lowerQuery = query.toLowerCase(); if (lowerQuery.startsWith('select')) { throw new McpError(ErrorCode.InvalidParams, 'Use execute_query for SELECT operations'); } if (!(lowerQuery.startsWith('insert') || lowerQuery.startsWith('update') || lowerQuery.startsWith('delete'))) { throw new McpError(ErrorCode.InvalidParams, 'Only INSERT, UPDATE, or DELETE operations are allowed with write_query'); } // Additional validation const validationError = validateQuery(query); if (validationError) { throw new McpError(ErrorCode.InvalidParams, validationError); } const connection = await this.configParser.getConnection(connectionId); if (!connection) { throw new McpError(ErrorCode.InvalidParams, `Connection not found: ${connectionId}`); } const result = await this.dbeaverClient.executeQuery(connection, query); const response = { query: query, connection: connection.name, executionTime: result.executionTime, affectedRows: result.rowCount, success: true }; return { content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2), }], }; } private async handleCreateTable(args: { connectionId: string; query: string }) { const connectionId = sanitizeConnectionId(args.connectionId); const query = args.query.trim(); if (!query.toLowerCase().startsWith('create table')) { throw new McpError(ErrorCode.InvalidParams, 'Only CREATE TABLE statements are allowed'); } const connection = await this.configParser.getConnection(connectionId); if (!connection) { throw new McpError(ErrorCode.InvalidParams, `Connection not found: ${connectionId}`); } const result = await this.dbeaverClient.executeQuery(connection, query); return { content: [{ type: 'text' as const, text: JSON.stringify({ success: true, message: 'Table created successfully', executionTime: result.executionTime }, null, 2), }], }; } private async handleAlterTable(args: { connectionId: string; query: string }) { const connectionId = sanitizeConnectionId(args.connectionId); const query = args.query.trim(); if (!query.toLowerCase().startsWith('alter table')) { throw new McpError(ErrorCode.InvalidParams, 'Only ALTER TABLE statements are allowed'); } const connection = await this.configParser.getConnection(connectionId); if (!connection) { throw new McpError(ErrorCode.InvalidParams, `Connection not found: ${connectionId}`); } const result = await this.dbeaverClient.executeQuery(connection, query); return { content: [{ type: 'text' as const, text: JSON.stringify({ success: true, message: 'Table altered successfully', executionTime: result.executionTime }, null, 2), }], }; } private async handleDropTable(args: { connectionId: string; tableName: string; confirm: boolean }) { const connectionId = sanitizeConnectionId(args.connectionId); const tableName = args.tableName; if (!tableName) { throw new McpError(ErrorCode.InvalidParams, 'Table name is required'); } if (!args.confirm) { return { content: [{ type: 'text' as const, text: JSON.stringify({ success: false, message: 'Safety confirmation required. Set confirm=true to proceed with dropping the table.' }, null, 2), }], }; } const connection = await this.configParser.getConnection(connectionId); if (!connection) { throw new McpError(ErrorCode.InvalidParams, `Connection not found: ${connectionId}`); } // Check if table exists first try { await this.dbeaverClient.getTableSchema(connection, tableName); } catch (error) { throw new McpError(ErrorCode.InvalidParams, `Table '${tableName}' does not exist or cannot be accessed`); } const dropQuery = `DROP TABLE "${tableName}"`; const result = await this.dbeaverClient.executeQuery(connection, dropQuery); return { content: [{ type: 'text' as const, text: JSON.stringify({ success: true, message: `Table '${tableName}' dropped successfully`, executionTime: result.executionTime }, null, 2), }], }; } private async handleGetTableSchema(args: { connectionId: string; tableName: string; includeIndexes?: boolean }) { const connectionId = sanitizeConnectionId(args.connectionId); const connection = await this.configParser.getConnection(connectionId); if (!connection) { throw new McpError(ErrorCode.InvalidParams, `Connection not found: ${connectionId}`); } const schema = await this.dbeaverClient.getTableSchema(connection, args.tableName); if (!args.includeIndexes) { (schema as any).indexes = undefined; } return { content: [{ type: 'text' as const, text: JSON.stringify(schema, null, 2), }], }; } private async handleExportData(args: { connectionId: string; query: string; format?: string; includeHeaders?: boolean; maxRows?: number }) { const connectionId = sanitizeConnectionId(args.connectionId); const query = args.query.trim(); // Validate query - only SELECT queries for export if (!query.toLowerCase().trimStart().startsWith('select')) { throw new McpError(ErrorCode.InvalidParams, 'Only SELECT queries are allowed for export'); } const connection = await this.configParser.getConnection(connectionId); if (!connection) { throw new McpError(ErrorCode.InvalidParams, `Connection not found: ${connectionId}`); } const maxRows = args.maxRows || 10000; const format = args.format || 'csv'; // Add LIMIT clause if not present let finalQuery = query; if (!query.toLowerCase().includes('limit')) { finalQuery = `${query} LIMIT ${maxRows}`; } const result = await this.dbeaverClient.executeQuery(connection, finalQuery); if (format === 'csv') { const csvData = convertToCSV(result.columns, result.rows); return { content: [{ type: 'text' as const, text: csvData, }], }; } else if (format === 'json') { const jsonData = result.rows.map(row => { const obj: any = {}; result.columns.forEach((col, idx) => { obj[col] = row[idx]; }); return obj; }); return { content: [{ type: 'text' as const, text: JSON.stringify(jsonData, null, 2), }], }; } else { throw new McpError(ErrorCode.InvalidParams, `Unsupported export format: ${format}. Use 'csv' or 'json'`); } } private async handleTestConnection(args: { connectionId: string }) { const connectionId = sanitizeConnectionId(args.connectionId); const connection = await this.configParser.getConnection(connectionId); if (!connection) { throw new McpError(ErrorCode.InvalidParams, `Connection not found: ${connectionId}`); } const testResult = await this.dbeaverClient.testConnection(connection); return { content: [{ type: 'text' as const, text: JSON.stringify(testResult, null, 2), }], }; } private async handleGetDatabaseStats(args: { connectionId: string }) { const connectionId = sanitizeConnectionId(args.connectionId); const connection = await this.configParser.getConnection(connectionId); if (!connection) { throw new McpError(ErrorCode.InvalidParams, `Connection not found: ${connectionId}`); } const stats = await this.dbeaverClient.getDatabaseStats(connection); return { content: [{ type: 'text' as const, text: JSON.stringify(stats, null, 2), }], }; } private async handleListTables(args: { connectionId: string; schema?: string; includeViews?: boolean }) { const connectionId = sanitizeConnectionId(args.connectionId); const connection = await this.configParser.getConnection(connectionId); if (!connection) { throw new McpError(ErrorCode.InvalidParams, `Connection not found: ${connectionId}`); } const tables = await this.dbeaverClient.listTables( connection, args.schema, args.includeViews || false ); return { content: [{ type: 'text' as const, text: JSON.stringify(tables, null, 2), }], }; } private async handleAppendInsight(args: { insight: string; connection?: string; tags?: string[] }) { if (!args.insight || args.insight.trim().length === 0) { throw new McpError(ErrorCode.InvalidParams, 'Insight text is required'); } const newInsight: BusinessInsight = { id: Date.now(), insight: args.insight.trim(), created_at: new Date().toISOString(), connection: args.connection, tags: args.tags || [] }; this.insights.push(newInsight); this.saveInsights(); return { content: [{ type: 'text' as const, text: JSON.stringify({ success: true, message: 'Insight added successfully', id: newInsight.id }, null, 2), }], }; } private async handleListInsights(args: { connection?: string; tags?: string[] }) { let filteredInsights = [...this.insights]; if (args.connection) { filteredInsights = filteredInsights.filter(insight => insight.connection === args.connection ); } if (args.tags && args.tags.length > 0) { filteredInsights = filteredInsights.filter(insight => insight.tags && args.tags!.some(tag => insight.tags!.includes(tag)) ); } return { content: [{ type: 'text' as const, text: JSON.stringify(filteredInsights, null, 2), }], }; } async run() { try { // Validate DBeaver workspace if (!this.configParser.isWorkspaceValid()) { this.log('DBeaver workspace not found. Please run DBeaver at least once to create the workspace.', 'error'); this.log('The server will start but will not be able to access any database connections.', 'error'); } const transport = new StdioServerTransport(); await this.server.connect(transport); this.log('DBeaver MCP server started successfully'); if (this.debug) { const debugInfo = this.configParser.getDebugInfo(); this.log(`Debug info: ${JSON.stringify(debugInfo, null, 2)}`, 'debug'); } } catch (error) { this.log(`Failed to start server: ${formatError(error)}`, 'error'); process.exit(1); } } } // Handle CLI arguments if (process.argv.includes('--help') || process.argv.includes('-h')) { console.error(` DBeaver MCP Server v${VERSION} Usage: dbeaver-mcp-server [options] Options: -h, --help Show this help message --version Show version information --debug Enable debug logging Environment Variables: DBEAVER_PATH Path to DBeaver executable DBEAVER_TIMEOUT Query timeout in milliseconds (default: 30000) DBEAVER_DEBUG Enable debug logging (true/false) Features: - Universal database support via DBeaver connections - Read and write operations with safety checks - Schema introspection and table management - Data export in multiple formats - Business insights tracking - Resource-based schema browsing For more information, visit: https://github.com/srthkdev/dbeaver-mcp-server `); process.exit(0); } if (process.argv.includes('--version')) { console.error(VERSION); process.exit(0); } // Start the server const server = new DBeaverMCPServer(); server.run().catch((error) => { console.error('Server startup failed:', error); process.exit(1); });

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