Skip to main content
Glama
mcp-memory-handler.ts12.5 kB
/** * MCP Memory Handler * Single responsibility: handle memory-related MCP requests using clean architecture */ import { DIContainer } from '../../container/di-container'; import { createErrorMessage } from '../../infrastructure/utilities'; import { MCPValidationError, MCPOperationError, MCPErrorCodes } from '../../infrastructure/errors'; export class McpMemoryHandler { private container: DIContainer; private initializationPromise: Promise<void> | null = null; constructor() { this.container = DIContainer.getInstance(); } /** * ZERO-FALLBACK: Thread-safe database initialization * Uses Promise-based singleton pattern to prevent race conditions */ private async ensureDatabaseInitialized(): Promise<void> { if (!this.initializationPromise) { this.initializationPromise = this.container.initializeDatabase() .catch((error) => { // Reset promise on failure to allow retry this.initializationPromise = null; throw error; }); } return this.initializationPromise; } async handleMemoryManage(request: { operation: 'create' | 'update' | 'delete'; memories?: any[]; updates?: any[]; identifiers?: string[]; }): Promise<any> { // Input validation - fail fast this.validateMemoryManageRequest(request); await this.ensureDatabaseInitialized(); const currentDb = this.container.getCurrentDatabase(); switch (request.operation) { case 'create': return this.handleCreateMemories(request.memories!, currentDb); case 'update': return this.handleUpdateMemories(request.updates!, currentDb); case 'delete': return this.handleDeleteMemories(request.identifiers!, currentDb); default: throw new MCPOperationError( `Invalid operation: ${request.operation}. Valid operations: create, update, delete`, MCPErrorCodes.VALIDATION_FAILED ); } } async handleMemoryRetrieve(identifiers: string[]): Promise<any> { // Input validation - fail fast this.validateIdentifiers(identifiers); await this.ensureDatabaseInitialized(); const currentDb = this.container.getCurrentDatabase(); const memoryRepo = this.container.getMemoryRepository(); const memories = await memoryRepo.findByIds(identifiers); return { memories: memories.map(memory => this.stripEmbeddings(memory)), _meta: { database: currentDb.database, retrieved: memories.length } }; } async handleMemorySearch( query: string, limit: number, includeGraphContext: boolean, memoryTypes?: string[], threshold?: number ): Promise<any> { // Input validation - fail fast this.validateSearchRequest(query, limit, threshold); await this.ensureDatabaseInitialized(); const currentDb = this.container.getCurrentDatabase(); const searchUseCase = this.container.getSearchMemoriesUseCase(); const results = await searchUseCase.execute({ query, limit, includeGraphContext, memoryTypes, threshold }); return { memories: results.map(result => ({ ...this.stripEmbeddings(result.memory), score: result.score })), _meta: { database: currentDb.database, total: results.length, query: query, queryTime: Date.now() } }; } private async handleCreateMemories(memories: any[], currentDb: any): Promise<any> { if (!memories || !Array.isArray(memories) || memories.length === 0) { throw new MCPValidationError( "memories array cannot be empty", MCPErrorCodes.EMPTY_ARRAY ); } const createUseCase = this.container.getCreateMemoryUseCase(); const results = []; for (const memoryInput of memories) { try { const memory = await createUseCase.execute(memoryInput); results.push({ id: memory.id, status: "created" }); } catch (error) { results.push({ id: memoryInput.name || "unknown", status: "failed", error: createErrorMessage("Failed to create memory", error) }); } } return { success: true, results, summary: { requested: memories.length, succeeded: results.filter(r => r.status === "created").length, failed: results.filter(r => r.status === "failed").length }, _meta: { database: currentDb.database, operation: "create", timestamp: new Date().toISOString() } }; } private async handleUpdateMemories(updates: any[], currentDb: any): Promise<any> { if (!updates || !Array.isArray(updates) || updates.length === 0) { throw new MCPValidationError( "updates array cannot be empty", MCPErrorCodes.EMPTY_ARRAY ); } const updateUseCase = this.container.getUpdateMemoryUseCase(); const results = []; for (const updateInput of updates) { try { const memory = await updateUseCase.execute(updateInput); results.push({ id: memory.id, status: "updated" }); } catch (error) { results.push({ id: updateInput.id || "unknown", status: "failed", error: createErrorMessage("Failed to update memory", error) }); } } return { success: true, results, summary: { requested: updates.length, succeeded: results.filter(r => r.status === "updated").length, failed: results.filter(r => r.status === "failed").length }, _meta: { database: currentDb.database, operation: "update", timestamp: new Date().toISOString() } }; } private async handleDeleteMemories(identifiers: string[], currentDb: any): Promise<any> { if (!identifiers || !Array.isArray(identifiers) || identifiers.length === 0) { throw new MCPValidationError( "identifiers array cannot be empty", MCPErrorCodes.EMPTY_ARRAY ); } const deleteUseCase = this.container.getDeleteMemoryUseCase(); const results = []; for (const id of identifiers) { try { await deleteUseCase.execute(id); results.push({ id, status: "deleted" }); } catch (error) { results.push({ id, status: "failed", error: createErrorMessage("Failed to delete memory", error) }); } } return { success: true, results, summary: { requested: identifiers.length, succeeded: results.filter(r => r.status === "deleted").length, failed: results.filter(r => r.status === "failed").length }, _meta: { database: currentDb.database, operation: "delete", timestamp: new Date().toISOString() } }; } private stripEmbeddings(memory: any): any { const { nameEmbedding, ...cleanMemory } = memory; return cleanMemory; } /** * Validate memory manage request structure * ZERO-FALLBACK: Invalid requests fail immediately */ private validateMemoryManageRequest(request: { operation: 'create' | 'update' | 'delete'; memories?: any[]; updates?: any[]; identifiers?: string[]; }): void { if (!request.operation) { throw new MCPValidationError( 'Operation is required', MCPErrorCodes.VALIDATION_FAILED ); } switch (request.operation) { case 'create': if (!request.memories || !Array.isArray(request.memories) || request.memories.length === 0) { throw new MCPValidationError( 'Create operation requires non-empty memories array', MCPErrorCodes.EMPTY_ARRAY ); } this.validateMemoryCreationData(request.memories); break; case 'update': if (!request.updates || !Array.isArray(request.updates) || request.updates.length === 0) { throw new MCPValidationError( 'Update operation requires non-empty updates array', MCPErrorCodes.EMPTY_ARRAY ); } this.validateMemoryUpdateData(request.updates); break; case 'delete': if (!request.identifiers || !Array.isArray(request.identifiers) || request.identifiers.length === 0) { throw new MCPValidationError( 'Delete operation requires non-empty identifiers array', MCPErrorCodes.EMPTY_ARRAY ); } this.validateIdentifiers(request.identifiers); break; } } /** * Validate memory creation data */ private validateMemoryCreationData(memories: any[]): void { for (let i = 0; i < memories.length; i++) { const memory = memories[i]; if (!memory || typeof memory !== 'object') { throw new MCPValidationError( `Memory at index ${i} must be an object`, MCPErrorCodes.VALIDATION_FAILED ); } if (!memory.name || typeof memory.name !== 'string' || memory.name.trim().length === 0) { throw new MCPValidationError( `Memory at index ${i} must have a non-empty name`, MCPErrorCodes.INVALID_NAME ); } if (!memory.memoryType || typeof memory.memoryType !== 'string' || memory.memoryType.trim().length === 0) { throw new MCPValidationError( `Memory at index ${i} must have a non-empty memoryType`, MCPErrorCodes.INVALID_TYPE ); } } } /** * Validate memory update data */ private validateMemoryUpdateData(updates: any[]): void { for (let i = 0; i < updates.length; i++) { const update = updates[i]; if (!update || typeof update !== 'object') { throw new MCPValidationError( `Update at index ${i} must be an object`, MCPErrorCodes.VALIDATION_FAILED ); } if (!update.id || typeof update.id !== 'string' || update.id.trim().length === 0) { throw new MCPValidationError( `Update at index ${i} must have a non-empty id`, MCPErrorCodes.INVALID_ID_FORMAT ); } if (update.id.length !== 18) { throw new MCPValidationError( `Update at index ${i} has invalid id format (expected 18 characters)`, MCPErrorCodes.INVALID_MEMORY_ID_LENGTH ); } } } /** * Validate identifiers array */ private validateIdentifiers(identifiers: string[]): void { if (!Array.isArray(identifiers)) { throw new MCPValidationError( 'Identifiers must be an array', MCPErrorCodes.VALIDATION_FAILED ); } if (identifiers.length === 0) { throw new MCPValidationError( 'Identifiers array cannot be empty', MCPErrorCodes.EMPTY_ARRAY ); } for (let i = 0; i < identifiers.length; i++) { const id = identifiers[i]; if (!id || typeof id !== 'string' || id.trim().length === 0) { throw new MCPValidationError( `Identifier at index ${i} must be a non-empty string`, MCPErrorCodes.INVALID_ID_FORMAT ); } if (id.length !== 18) { throw new MCPValidationError( `Identifier at index ${i} has invalid format (expected 18 characters)`, MCPErrorCodes.INVALID_MEMORY_ID_LENGTH ); } } } /** * Validate search request parameters */ private validateSearchRequest(query: string, limit: number, threshold?: number): void { if (!query || typeof query !== 'string' || query.trim().length === 0) { throw new MCPValidationError( 'Search query must be a non-empty string', MCPErrorCodes.EMPTY_QUERY ); } if (!Number.isInteger(limit) || limit <= 0) { throw new MCPValidationError( 'Search limit must be a positive integer', MCPErrorCodes.INVALID_LIMIT ); } if (limit > 1000) { throw new MCPValidationError( 'Search limit cannot exceed 1000', MCPErrorCodes.INVALID_LIMIT ); } if (threshold !== undefined) { if (typeof threshold !== 'number' || threshold < 0 || threshold > 1) { throw new MCPValidationError( 'Search threshold must be a number between 0 and 1', MCPErrorCodes.INVALID_THRESHOLD ); } } } }

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/sylweriusz/mcp-neo4j-memory-server'

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