UNPKG

@memory-bank/mcp

Version:

Memory-enabled Co-Pilot (MCP) server for managing project documentation and context

187 lines 8.87 kB
import { logger } from '../../shared/utils/logger.js'; // import { DocumentRepositorySelector } from '../../application/services/DocumentRepositorySelector.js'; import { ApplicationErrors } from '../../shared/errors/ApplicationError.js'; import { BaseError } from '../../shared/errors/BaseError.js'; /** * Unified controller for document operations in both branch and global scopes. * Uses scope parameter to determine which repository and use case to use. */ export class DocumentController { readBranchDocumentUseCase; writeBranchDocumentUseCase; readGlobalDocumentUseCase; writeGlobalDocumentUseCase; presenter; configProvider; componentLogger = logger.withContext({ component: 'DocumentController' }); /** * Constructor * @param readBranchDocumentUseCase Use case for reading branch documents * @param writeBranchDocumentUseCase Use case for writing branch documents * @param readGlobalDocumentUseCase Use case for reading global documents * @param writeGlobalDocumentUseCase Use case for writing global documents * @param presenter Response presenter */ constructor(readBranchDocumentUseCase, writeBranchDocumentUseCase, readGlobalDocumentUseCase, writeGlobalDocumentUseCase, // private readonly repositorySelector: DocumentRepositorySelector, // Not used currently presenter, configProvider) { this.readBranchDocumentUseCase = readBranchDocumentUseCase; this.writeBranchDocumentUseCase = writeBranchDocumentUseCase; this.readGlobalDocumentUseCase = readGlobalDocumentUseCase; this.writeGlobalDocumentUseCase = writeGlobalDocumentUseCase; this.presenter = presenter; this.configProvider = configProvider; } /** * Read a document from either branch or global memory bank * @param params Parameters for reading document * @param params.scope 'branch' or 'global' scope * @param params.branchName Optional branch name (auto-detected in project mode if not provided) * @param params.path Document path * @returns Promise resolving to MCP response */ async readDocument(params) { const { scope, branchName, path } = params; try { this.componentLogger.info('Reading document', { operation: 'readDocument', scope, branchName, path }); if (scope === 'global') { // Read from global memory bank const document = await this.readGlobalDocumentUseCase.execute({ path }); return this.presenter.presentSuccess(document); } else if (scope === 'branch') { // Read from branch memory bank const document = await this.readBranchDocumentUseCase.execute({ branchName, path }); return this.presenter.presentSuccess(document); } else { throw ApplicationErrors.invalidInput(`Invalid scope: ${scope}. Must be 'branch' or 'global'.`); } } catch (error) { this.componentLogger.error('Failed to read document', { operation: 'readDocument', scope, branchName, path, error }); return this.handleError(error); } } /** * Write a document to either branch or global memory bank * @param params Parameters for writing document * @param params.scope 'branch' or 'global' scope * @param params.branchName Optional branch name (auto-detected in project mode if not provided) * @param params.path Document path * @param params.content Document content (optional, mutually exclusive with patches) * @param params.patches JSON Patch operations (optional, mutually exclusive with content) * @param params.tags Document tags (optional) * @param params.returnContent Whether to return document content in response (default: false) * @returns Promise resolving to MCP response */ async writeDocument(params) { const { scope, branchName, path, content, patches, tags, returnContent } = params; try { // Determine content conditions const hasPatches = patches && Array.isArray(patches) && patches.length > 0; const hasContent = content !== undefined && content !== null && content !== ''; this.componentLogger.info('Writing document', { operation: 'writeDocument', scope, branchName, path, hasContent, hasPatches, hasTags: tags && tags.length > 0 }); // Validate content/patches exclusivity if (hasContent && hasPatches) { const error = ApplicationErrors.invalidInput('Cannot provide both content and patches simultaneously'); // Important: Throw the error directly for integration tests compatibility throw new Error(error.message); } // Validate that either content or patches are provided if (!hasContent && !hasPatches) { const error = ApplicationErrors.invalidInput('Either document content or patches must be provided'); // Important: Throw the error directly for integration tests compatibility throw new Error(error.message); } if (scope === 'global') { // Write to global memory bank const result = await this.writeGlobalDocumentUseCase.execute({ document: { path, content: hasContent ? content : undefined, tags }, patches: hasPatches ? patches : undefined, returnContent }); return this.presenter.presentSuccess(result.document); } else if (scope === 'branch') { // Check if branch name is required but not provided // For integration test compatibility: we should throw directly here // But only when isProjectMode is false (auto-detect should work in project mode) if (!branchName) { if (this.configProvider) { // Get the config to check if we're in project mode const config = this.configProvider.getConfig(); if (!config.isProjectMode) { // Only throw if we're not in project mode throw new Error('Branch name is required when not running in project mode'); } } else { // If configProvider is not available, always throw error for missing branch name throw new Error('Branch name is required when not running in project mode'); } } // Write to branch memory bank const result = await this.writeBranchDocumentUseCase.execute({ branchName, document: { path, content: hasContent ? content : undefined, tags }, patches: hasPatches ? patches : undefined, returnContent }); return this.presenter.presentSuccess(result.document); } else { throw ApplicationErrors.invalidInput(`Invalid scope: ${scope}. Must be 'branch' or 'global'.`); } } catch (error) { this.componentLogger.error('Failed to write document', { operation: 'writeDocument', scope, branchName, path, error }); // If it's already an Error instance with a message, rethrow it directly for test compatibility if (error instanceof Error) { throw error; } return this.handleError(error); } } /** * Handle errors and convert them to MCP responses * @param error Error to handle * @returns MCP response with error information */ handleError(error) { if (error instanceof BaseError) { return this.presenter.presentError(error); } // Convert unknown errors to ApplicationError const applicationError = ApplicationErrors.unexpectedControllerError('DocumentController', error instanceof Error ? error : undefined); return this.presenter.presentError(applicationError); } } //# sourceMappingURL=DocumentController.js.map