UNPKG

@mseep/atlas-mcp-server

Version:

A Model Context Protocol (MCP) server for ATLAS, a Neo4j-powered task management system for LLM Agents - implementing a three-tier architecture (Projects, Tasks, Knowledge) to manage complex workflows.

216 lines (215 loc) 8.37 kB
import neo4j from 'neo4j-driver'; import { config } from '../../config/index.js'; import { logger } from '../../utils/logger.js'; import { databaseEvents, DatabaseEventType } from './events.js'; import { exportDatabase } from './backupRestoreService.js'; // Import the export function for backup trigger /** * Neo4j connection management singleton * Responsible for creating and managing the Neo4j driver connection */ class Neo4jDriver { constructor() { this.driver = null; this.connectionPromise = null; this.transactionCounter = 0; } /** * Get the Neo4jDriver singleton instance */ static getInstance() { if (!Neo4jDriver.instance) { Neo4jDriver.instance = new Neo4jDriver(); } return Neo4jDriver.instance; } /** * Initialize the Neo4j driver connection * @returns Promise that resolves to the Neo4j driver */ async initDriver() { if (this.driver) { return this.driver; } try { const { neo4jUri, neo4jUser, neo4jPassword } = config; if (!neo4jUri || !neo4jUser || !neo4jPassword) { throw new Error('Neo4j connection details are not properly configured'); } logger.info('Initializing Neo4j driver connection'); this.driver = neo4j.driver(neo4jUri, neo4j.auth.basic(neo4jUser, neo4jPassword), { maxConnectionLifetime: 3 * 60 * 60 * 1000, // 3 hours maxConnectionPoolSize: 50, connectionAcquisitionTimeout: 2 * 60 * 1000, // 2 minutes disableLosslessIntegers: true // Recommended for JS compatibility }); // Verify connection await this.driver.verifyConnectivity(); logger.info('Neo4j driver connection established successfully'); return this.driver; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error('Failed to initialize Neo4j driver', { error: errorMessage }); throw new Error(`Failed to initialize Neo4j connection: ${errorMessage}`); } } /** * Get the Neo4j driver instance, initializing it if necessary * @returns Promise that resolves to the Neo4j driver */ async getDriver() { if (!this.connectionPromise) { this.connectionPromise = this.initDriver(); } return this.connectionPromise; } /** * Create a new Neo4j session * @param database Optional database name * @returns Promise that resolves to a new Neo4j session */ async getSession(database) { const driver = await this.getDriver(); // Use the default database configured for the driver instance // Neo4j Community Edition typically uses 'neo4j' or potentially 'system' // Passing undefined lets the driver use its default. return driver.session({ database: database || undefined, defaultAccessMode: neo4j.session.WRITE }); } /** * Execute a query with a transaction * @param cypher Cypher query to execute * @param params Parameters for the query * @param database Optional database name * @returns Promise that resolves to the query result records */ async executeQuery(cypher, params = {}, database) { const session = await this.getSession(database); try { const result = await session.executeWrite(async (tx) => { const queryResult = await tx.run(cypher, params); return queryResult.records; }); // Publish write operation event // Publish write operation event this.publishWriteOperation({ query: cypher, params }); // Removed: Trigger background backup after successful write // this.triggerBackgroundBackup(); // This was inefficient return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error('Error executing Neo4j query', { error: errorMessage, query: cypher, // Avoid logging potentially sensitive params directly in production // params: JSON.stringify(params) }); // Publish error event databaseEvents.publish(DatabaseEventType.ERROR, { timestamp: new Date().toISOString(), operation: 'executeQuery', error: errorMessage, query: cypher }); throw error; // Re-throw the original error } finally { await session.close(); } } /** * Execute a read-only query * @param cypher Cypher query to execute * @param params Parameters for the query * @param database Optional database name * @returns Promise that resolves to the query result records */ async executeReadQuery(cypher, params = {}, database) { const session = await this.getSession(database); try { const result = await session.executeRead(async (tx) => { const queryResult = await tx.run(cypher, params); return queryResult.records; }); // Publish read operation event databaseEvents.publish(DatabaseEventType.READ_OPERATION, { timestamp: new Date().toISOString(), query: cypher }); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error('Error executing Neo4j read query', { error: errorMessage, query: cypher, // params: JSON.stringify(params) }); // Publish error event databaseEvents.publish(DatabaseEventType.ERROR, { timestamp: new Date().toISOString(), operation: 'executeReadQuery', error: errorMessage, query: cypher }); throw error; // Re-throw the original error } finally { await session.close(); } } /** * Publish a database write operation event * @param operation Details about the operation * @private */ publishWriteOperation(operation) { this.transactionCounter++; databaseEvents.publish(DatabaseEventType.WRITE_OPERATION, { timestamp: new Date().toISOString(), transactionId: this.transactionCounter, operation }); } /** * Triggers a database backup in the background, including rotation logic. * Logs errors but does not throw to avoid interrupting the main flow. * @private */ triggerBackgroundBackup() { logger.debug('Triggering background database backup with rotation...'); // Run backup in the background without awaiting it exportDatabase() .then(backupPath => { logger.info(`Background database backup successful: ${backupPath}`); }) .catch(error => { const errorMessage = error instanceof Error ? error.message : String(error); logger.error('Background database backup failed:', { error: errorMessage }); // Consider adding more robust error handling/notification if needed }); } /** * Close the Neo4j driver connection */ async close() { if (this.driver) { try { await this.driver.close(); this.driver = null; this.connectionPromise = null; logger.info('Neo4j driver connection closed'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error('Error closing Neo4j driver connection', { error: errorMessage }); throw error; // Re-throw the error to propagate it } } } } // Export the singleton instance export const neo4jDriver = Neo4jDriver.getInstance();