@memory-bank/mcp
Version:
Memory-enabled Co-Pilot (MCP) server for managing project documentation and context
187 lines • 8.87 kB
JavaScript
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