@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
JavaScript
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();