Skip to main content
Glama

atlas-mcp-server

utils.ts14.1 kB
import { logger, requestContextService } from "../../utils/index.js"; // Updated import path import { neo4jDriver } from "./driver.js"; import { NodeLabels, PaginatedResult, PaginationOptions, RelationshipTypes, } from "./types.js"; import { Record as Neo4jRecord } from "neo4j-driver"; // Import Record type /** * Database utility functions for Neo4j */ export class Neo4jUtils { /** * Initialize the Neo4j database schema with constraints and indexes * Should be called at application startup */ static async initializeSchema(): Promise<void> { const session = await neo4jDriver.getSession(); const reqContext = requestContextService.createRequestContext({ operation: "Neo4jUtils.initializeSchema", }); try { logger.info("Initializing Neo4j database schema", reqContext); const constraints = [ `CREATE CONSTRAINT project_id_unique IF NOT EXISTS FOR (p:${NodeLabels.Project}) REQUIRE p.id IS UNIQUE`, `CREATE CONSTRAINT task_id_unique IF NOT EXISTS FOR (t:${NodeLabels.Task}) REQUIRE t.id IS UNIQUE`, `CREATE CONSTRAINT knowledge_id_unique IF NOT EXISTS FOR (k:${NodeLabels.Knowledge}) REQUIRE k.id IS UNIQUE`, `CREATE CONSTRAINT user_id_unique IF NOT EXISTS FOR (u:${NodeLabels.User}) REQUIRE u.id IS UNIQUE`, `CREATE CONSTRAINT citation_id_unique IF NOT EXISTS FOR (c:${NodeLabels.Citation}) REQUIRE c.id IS UNIQUE`, `CREATE CONSTRAINT tasktype_name_unique IF NOT EXISTS FOR (t:${NodeLabels.TaskType}) REQUIRE t.name IS UNIQUE`, `CREATE CONSTRAINT domain_name_unique IF NOT EXISTS FOR (d:${NodeLabels.Domain}) REQUIRE d.name IS UNIQUE`, ]; const indexes = [ `CREATE INDEX project_status IF NOT EXISTS FOR (p:${NodeLabels.Project}) ON (p.status)`, `CREATE INDEX project_taskType IF NOT EXISTS FOR (p:${NodeLabels.Project}) ON (p.taskType)`, `CREATE INDEX task_status IF NOT EXISTS FOR (t:${NodeLabels.Task}) ON (t.status)`, `CREATE INDEX task_priority IF NOT EXISTS FOR (t:${NodeLabels.Task}) ON (t.priority)`, `CREATE INDEX task_projectId IF NOT EXISTS FOR (t:${NodeLabels.Task}) ON (t.projectId)`, `CREATE INDEX knowledge_projectId IF NOT EXISTS FOR (k:${NodeLabels.Knowledge}) ON (k.projectId)`, ]; // Full-text indexes (check compatibility with Community Edition version) // These might require specific configuration or versions. Wrap in try-catch if needed. const fullTextIndexes = [ `CREATE FULLTEXT INDEX project_fulltext IF NOT EXISTS FOR (p:${NodeLabels.Project}) ON EACH [p.name, p.description]`, `CREATE FULLTEXT INDEX task_fulltext IF NOT EXISTS FOR (t:${NodeLabels.Task}) ON EACH [t.title, t.description]`, `CREATE FULLTEXT INDEX knowledge_fulltext IF NOT EXISTS FOR (k:${NodeLabels.Knowledge}) ON EACH [k.text]`, ]; // Execute schema creation queries within a transaction await session.executeWrite(async (tx) => { for (const query of [...constraints, ...indexes, ...fullTextIndexes]) { try { await tx.run(query); } catch (error) { // Log index creation errors but don't necessarily fail initialization // Especially full-text might not be supported/enabled const errorMessage = error instanceof Error ? error.message : String(error); if (query.includes("FULLTEXT")) { logger.warning( `Could not create full-text index (potentially unsupported): ${errorMessage}. Query: ${query}`, { ...reqContext, queryFailed: query }, ); } else { logger.error( `Failed to execute schema query: ${errorMessage}. Query: ${query}`, error as Error, { ...reqContext, queryFailed: query }, ); // Rethrow for critical constraints/indexes if (!query.includes("FULLTEXT")) throw error; } } } }); logger.info("Neo4j database schema initialization attempted", reqContext); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error( "Failed to initialize Neo4j database schema", error as Error, { ...reqContext, detail: errorMessage }, ); throw error; } finally { await session.close(); } } /** * Clear all data from the database and reinitialize the schema * WARNING: This permanently deletes all data */ static async clearDatabase(): Promise<void> { const session = await neo4jDriver.getSession(); const reqContext = requestContextService.createRequestContext({ operation: "Neo4jUtils.clearDatabase", }); try { logger.warning("Clearing all data from Neo4j database", reqContext); // Delete all nodes and relationships await session.executeWrite(async (tx) => { await tx.run("MATCH (n) DETACH DELETE n"); }); // Recreate schema await this.initializeSchema(); logger.info("Neo4j database cleared successfully", reqContext); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error("Failed to clear Neo4j database", error as Error, { ...reqContext, detail: errorMessage, }); throw error; } finally { await session.close(); } } /** * Apply pagination to query results * @param data Array of data to paginate * @param options Pagination options * @returns Paginated result object */ static paginateResults<T>( data: T[], options: PaginationOptions = {}, ): PaginatedResult<T> { const page = Math.max(options.page || 1, 1); const limit = Math.min(Math.max(options.limit || 20, 1), 100); // Ensure limit is between 1 and 100 const total = data.length; const totalPages = Math.ceil(total / limit); const startIndex = (page - 1) * limit; const endIndex = Math.min(startIndex + limit, total); // Ensure endIndex doesn't exceed total const paginatedData = data.slice(startIndex, endIndex); return { data: paginatedData, total: total, page: page, limit: limit, totalPages: totalPages, }; } /** * Generate a Cypher fragment for array parameters (e.g., for IN checks) * @param nodeAlias Alias of the node in the query (e.g., 't' for task) * @param propertyName Name of the property on the node (e.g., 'tags') * @param paramName Name for the Cypher parameter (e.g., 'tagsList') * @param arrayParam Array parameter value * @param matchAll If true, use ALL items must be in the node's list. If false (default), use ANY item must be in the node's list. * @returns Object with cypher fragment and params */ static generateArrayInListQuery( nodeAlias: string, propertyName: string, paramName: string, arrayParam?: string[] | string, matchAll: boolean = false, ): { cypher: string; params: Record<string, any> } { if (!arrayParam || (Array.isArray(arrayParam) && arrayParam.length === 0)) { return { cypher: "", params: {} }; } const params: Record<string, any> = {}; const listParam = Array.isArray(arrayParam) ? arrayParam : [arrayParam]; params[paramName] = listParam; const operator = matchAll ? "ALL" : "ANY"; // Cypher syntax for checking if items from a parameter list are in a node's list property const cypher = `${operator}(item IN $${paramName} WHERE item IN ${nodeAlias}.${propertyName})`; return { cypher, params }; } /** * Validate that a node exists in the database * @param label Node label * @param property Property to check * @param value Value to check * @returns True if the node exists, false otherwise */ static async nodeExists( label: NodeLabels, property: string, value: string | number, // Allow number for potential future use ): Promise<boolean> { const session = await neo4jDriver.getSession(); try { // Use EXISTS for potentially better performance than COUNT const query = ` MATCH (n:${label} {${property}: $value}) RETURN EXISTS { (n) } AS nodeExists `; const result = await session.executeRead(async (tx) => { const res = await tx.run(query, { value }); return res.records[0]?.get("nodeExists"); }); return result === true; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorContext = requestContextService.createRequestContext({ operation: "Neo4jUtils.nodeExists", label, property, value, }); logger.error( `Error checking node existence for ${label} {${property}: ${value}}`, error as Error, { ...errorContext, detail: errorMessage }, ); throw error; // Re-throw error after logging } finally { await session.close(); } } /** * Validate relationships between nodes * @param startLabel Label of the start node * @param startProperty Property of the start node to check * @param startValue Value of the start node property * @param endLabel Label of the end node * @param endProperty Property of the end node to check * @param endValue Value of the end node property * @param relationshipType Type of relationship to check * @returns True if the relationship exists, false otherwise */ static async relationshipExists( startLabel: NodeLabels, startProperty: string, startValue: string | number, endLabel: NodeLabels, endProperty: string, endValue: string | number, relationshipType: RelationshipTypes, ): Promise<boolean> { const session = await neo4jDriver.getSession(); try { // Use EXISTS for potentially better performance const query = ` MATCH (a:${startLabel} {${startProperty}: $startValue}) MATCH (b:${endLabel} {${endProperty}: $endValue}) RETURN EXISTS { (a)-[:${relationshipType}]->(b) } AS relExists `; const result = await session.executeRead(async (tx) => { const res = await tx.run(query, { startValue, endValue }); return res.records[0]?.get("relExists"); }); return result === true; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorContext = requestContextService.createRequestContext({ operation: "Neo4jUtils.relationshipExists", startLabel, startProperty, startValue, endLabel, endProperty, endValue, relationshipType, }); logger.error( `Error checking relationship existence: (${startLabel})-[:${relationshipType}]->(${endLabel})`, error as Error, { ...errorContext, detail: errorMessage }, ); throw error; } finally { await session.close(); } } /** * Generate a timestamp string in ISO format for database operations * @returns Current timestamp as ISO string */ static getCurrentTimestamp(): string { return new Date().toISOString(); } /** * Process Neo4j result records into plain JavaScript objects. * Assumes the record contains the node or properties under the specified key. * @param records Neo4j result records array (RecordShape from neo4j-driver). * @param primaryKey The key in the record containing the node or properties map (default: 'n'). * @returns Processed records as an array of plain objects. */ static processRecords<T>( records: Neo4jRecord[], primaryKey: string = "n", ): T[] { if (!records || records.length === 0) { return []; } return records .map((record) => { // Use .toObject() which handles conversion from Neo4j types const obj = record.toObject(); // If the query returns the node directly (e.g., RETURN n), access its properties // If the query returns properties directly (e.g., RETURN n.id as id), obj already has them. const data = obj[primaryKey]?.properties ? obj[primaryKey].properties : obj; // Ensure 'urls' is an array if it exists (handles potential null/undefined from DB) if (data && "urls" in data) { data.urls = data.urls || []; } // Ensure 'tags' is an array if it exists if (data && "tags" in data) { data.tags = data.tags || []; } // Ensure 'citations' is an array if it exists if (data && "citations" in data) { data.citations = data.citations || []; } return data as T; }) .filter((item): item is T => item !== null && item !== undefined); } /** * Check if the database is empty (no nodes exist) * @returns Promise<boolean> - true if database is empty, false otherwise */ static async isDatabaseEmpty(): Promise<boolean> { const session = await neo4jDriver.getSession(); try { const query = ` MATCH (n) RETURN count(n) = 0 AS isEmpty LIMIT 1 `; const result = await session.executeRead(async (tx) => { const res = await tx.run(query); // If no records are returned (e.g., DB error), assume not empty for safety return res.records[0]?.get("isEmpty") ?? false; }); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorContext = requestContextService.createRequestContext({ operation: "Neo4jUtils.isDatabaseEmpty.error", }); logger.error("Error checking if database is empty", error as Error, { ...errorContext, detail: errorMessage, }); // If we can't check, assume it's not empty to be safe return false; } finally { await session.close(); } } }

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/cyanheads/atlas-mcp-server'

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