@memory-bank/mcp
Version:
Memory-enabled Co-Pilot (MCP) server for managing project documentation and context
385 lines • 18.5 kB
JavaScript
import { ApplicationError, ApplicationErrorCodes } from "../../shared/errors/ApplicationError.js"; // ★ ApplicationError と Codes をインポート
import { BaseError } from "../../shared/errors/BaseError.js";
import { DomainError, DomainErrors } from "../../shared/errors/DomainError.js";
import { logger } from "../../shared/utils/logger.js";
/**
* Controller for global memory bank operations
* Handles incoming requests related to global memory bank
*/
export class GlobalController {
readGlobalDocumentUseCase;
writeGlobalDocumentUseCase;
searchDocumentsByTagsUseCase;
updateTagIndexUseCase;
presenter;
configProvider;
_type = 'controller';
componentLogger = logger.withContext({ component: 'GlobalController' });
/**
* Constructor
* @param readGlobalDocumentUseCase Use case for reading global documents
* @param writeGlobalDocumentUseCase Use case for writing global documents
* @param searchDocumentsByTagsUseCase Use case for searching documents by tags
* @param updateTagIndexUseCase Use case for updating tag index
* @param presenter Response presenter
* @param options Optional dependencies like JSON document use cases and V2 tag index
*/
updateTagIndexUseCaseV2;
readJsonDocumentUseCase;
writeJsonDocumentUseCase;
deleteJsonDocumentUseCase;
searchJsonDocumentsUseCase;
updateJsonIndexUseCase;
constructor(readGlobalDocumentUseCase, writeGlobalDocumentUseCase, searchDocumentsByTagsUseCase, updateTagIndexUseCase, presenter, configProvider, options) {
this.readGlobalDocumentUseCase = readGlobalDocumentUseCase;
this.writeGlobalDocumentUseCase = writeGlobalDocumentUseCase;
this.searchDocumentsByTagsUseCase = searchDocumentsByTagsUseCase;
this.updateTagIndexUseCase = updateTagIndexUseCase;
this.presenter = presenter;
this.configProvider = configProvider;
this.updateTagIndexUseCaseV2 = options?.updateTagIndexUseCaseV2;
this.readJsonDocumentUseCase = options?.readJsonDocumentUseCase;
this.writeJsonDocumentUseCase = options?.writeJsonDocumentUseCase;
this.deleteJsonDocumentUseCase = options?.deleteJsonDocumentUseCase;
this.searchJsonDocumentsUseCase = options?.searchJsonDocumentsUseCase;
this.updateJsonIndexUseCase = options?.updateJsonIndexUseCase;
}
/**
* Read document from global memory bank
* @param path Document path
* @returns Promise resolving to MCP response with document
*/
async readDocument(path) {
try {
this.componentLogger.info(`Reading global document`, { operation: 'readDocument', path });
const result = await this.readGlobalDocumentUseCase.execute({ path });
return this.presenter.presentSuccess(result.document); // Already correct, no change needed here
}
catch (error) {
return this.handleError(error, 'readDocument');
}
}
/**
* Write document to global memory bank
* @param path Document path
* @param content Document content
* @param tags Optional tags for the document
* @returns Promise resolving to MCP response with the result
*/
async writeDocument(params) {
const { path: docPath, content, patches, tags: tagStrings } = params; // patches を追加
try {
this.componentLogger.info(`Writing global document`, { operation: 'writeDocument', docPath, hasContent: !!content, hasPatches: !!patches }); // ログに情報追加
// content と patches の排他チェック (ユースケースでもやるけど、コントローラーでもやるのが親切)
const hasContent = content !== undefined && content !== null;
const hasPatches = patches !== undefined && patches !== null;
if (!hasContent && !hasPatches) {
throw new ApplicationError(ApplicationErrorCodes.INVALID_INPUT, 'Either content or patches must be provided');
}
if (hasContent && hasPatches) {
throw new ApplicationError(ApplicationErrorCodes.INVALID_INPUT, 'Cannot provide both content and patches');
}
// ユースケースに渡す document オブジェクトを構築
// ユースケースに渡す document オブジェクトを構築
// WriteDocumentDTO には content が必須だが、patches を使う場合は content は不要。
// ユースケース側で patches を any キャストで受け取っているので、それに合わせる。
let documentInput;
if (hasContent) {
// content がある場合は WriteDocumentDTO 型
documentInput = {
path: docPath,
content: content, // content は必須なので設定
tags: tagStrings || [],
};
}
else if (hasPatches) {
// patches がある場合は、content を含まず patches を持つオブジェクトを作成
// ユースケース側で any キャストされることを想定
documentInput = {
path: docPath,
tags: tagStrings || [],
patches: patches, // patches を設定
};
}
else {
// このケースは上のチェックで弾かれるはず
throw new Error('Invalid state: No content or patches');
}
// Execute the use case and store the result
const result = await this.writeGlobalDocumentUseCase.execute({
// documentInput は WriteDocumentDTO | { ... patches ... } 型だが、
// ユースケースの execute は WriteGlobalDocumentInput (document: WriteDocumentDTO) を期待している。
// しかし、ユースケース内部で patches を any キャストで扱っているので、このまま渡す。
// (より厳密にするならユースケースの型定義を見直すべきだが、既存実装に合わせる)
document: documentInput, // ユースケースの型定義に合わせるためのキャスト
});
// Pass the result from the use case to the presenter
return this.presenter.presentSuccess(result);
}
catch (error) {
return this.handleError(error, 'writeDocument');
}
}
/**
* Read core files from global memory bank
* @returns Promise resolving to MCP response with core files
*/
async readCoreFiles() {
try {
this.componentLogger.info('Reading global core files', { operation: 'readCoreFiles' });
const coreFiles = [
'architecture.json',
'coding-standards.json',
'domain-models.json',
'glossary.json',
'tech-stack.json',
'user-guide.json',
];
const result = {};
for (const documentPath of coreFiles) {
try {
const docResult = await this.readGlobalDocumentUseCase.execute({ path: documentPath });
if (docResult && docResult.document) {
result[documentPath.replace('.json', '')] = docResult.document;
}
else {
result[documentPath.replace('.json', '')] = {
path: documentPath,
content: '',
tags: ['global', 'core', documentPath.replace('.json', '')],
lastModified: new Date().toISOString(),
};
}
}
catch (error) {
this.componentLogger.error(`Error reading global core file`, { operation: 'readCoreFiles', documentPath, error });
result[documentPath.replace('.json', '')] = {
path: documentPath,
content: '',
tags: ['global', 'core', documentPath.replace('.json', '')],
lastModified: new Date().toISOString(),
};
}
}
return this.presenter.presentSuccess(result); // Already correct, no change needed here
}
catch (error) {
return this.handleError(error, 'readCoreFiles');
}
}
/**
* Update tags index in global memory bank
* @returns Promise resolving to MCP response with the result
*/
async updateTagsIndex() {
try {
this.componentLogger.info('Updating global tags index', { operation: 'updateTagsIndex' });
if (this.updateTagIndexUseCaseV2) {
this.componentLogger.info('Using UpdateTagIndexUseCaseV2 for global tag index update', { operation: 'updateTagsIndex' });
const result = await this.updateTagIndexUseCaseV2.execute({
branchName: undefined, // Global memory bank
fullRebuild: true,
});
return this.presenter.presentSuccess(result); // Already correct, no change needed here
}
else {
this.componentLogger.info('Using UpdateTagIndexUseCase (V1) for global tag index update', { operation: 'updateTagsIndex' });
const result = await this.updateTagIndexUseCase.execute({
branchName: undefined, // Global memory bank
fullRebuild: true,
});
return this.presenter.presentSuccess(result); // Already correct, no change needed here
}
}
catch (error) {
return this.handleError(error, 'updateTagsIndex');
}
}
// Remove the old findDocumentsByTags method
/**
* Search documents by tags in memory banks
* @param input Search parameters (tags, match, scope, branch, docs)
* @returns Promise resolving to MCP response with search results
*/
async searchDocumentsByTags(input) {
try {
this.componentLogger.info(`Searching documents by tags`, { operation: 'searchDocumentsByTags', input });
// Ensure 'docs' path is provided, default scope to 'global' if not specified and branchName is absent
const searchInput = {
...input,
scope: input.scope ?? (input.branchName ? 'all' : 'global'), // Default scope logic
docs: this.configProvider.getConfig().docsRoot // Use injected configProvider
};
if (!searchInput.docs) {
throw new Error("Docs path is missing in configuration or input.");
}
// Pass the correct input structure to the use case
// Pass the correct input structure including docs path
const result = await this.searchDocumentsByTagsUseCase.execute(searchInput);
this.componentLogger.info(`Search completed`, { operation: 'searchDocumentsByTags', count: result.results.length });
return this.presenter.presentSuccess(result);
}
catch (error) {
return this.handleError(error, 'searchDocumentsByTags');
}
}
/**
* Handle errors in controller methods
* @param error Error to handle
* @returns Formatted error response
*/
handleError(error, operation) {
this.componentLogger.error('Error details:', {
operation, // Add operation context
errorType: error.constructor.name,
message: error.message,
code: error.code,
details: error.details,
stack: error instanceof Error ? error.stack : undefined
});
if (error instanceof BaseError || error instanceof DomainError) {
return this.presenter.presentError(error);
}
if (DomainErrors.unexpectedError) {
return this.presenter.presentError(DomainErrors.unexpectedError(error instanceof Error ? error.message : 'An unexpected error occurred', { originalError: error }));
}
else {
return this.presenter.presentError(new DomainError('UNEXPECTED_ERROR', error instanceof Error ? error.message : 'An unexpected error occurred', { originalError: error }));
}
}
/**
* Read JSON document from global memory bank
* @param options Options for reading document (path or ID)
* @returns Promise resolving to MCP response with JSON document
*/
async readJsonDocument(options) {
try {
this.componentLogger.info(`Reading global JSON document`, { operation: 'readJsonDocument', path: options.path, id: options.id });
if (!this.readJsonDocumentUseCase) {
throw DomainErrors.featureNotAvailable('JSON document features');
}
const result = await this.readJsonDocumentUseCase.execute({
branchName: undefined, // Global memory bank
path: options.path,
id: options.id,
});
return this.presenter.presentSuccess(result.document); // Already correct, no change needed here
}
catch (error) {
return this.handleError(error, 'readJsonDocument');
}
}
/**
* Write JSON document to global memory bank
* @param document Document data to write
* @returns Promise resolving to MCP response with the result
*/
async writeJsonDocument(document) {
try {
this.componentLogger.info(`Writing global JSON document`, { operation: 'writeJsonDocument', path: document.path });
if (!this.writeJsonDocumentUseCase) {
throw DomainErrors.featureNotAvailable('JSON document features');
}
const result = await this.writeJsonDocumentUseCase.execute({
branchName: undefined, // Global memory bank
document: {
path: document.path || '',
title: document.title,
documentType: document.documentType,
content: document.content,
tags: document.tags,
},
});
return this.presenter.presentSuccess(result); // Already correct, no change needed here
}
catch (error) {
return this.handleError(error, 'writeJsonDocument');
}
}
/**
* Delete JSON document from global memory bank
* @param options Options for deleting document (path or ID)
* @returns Promise resolving to MCP response with the result
*/
async deleteJsonDocument(options) {
try {
this.componentLogger.info(`Deleting global JSON document`, { operation: 'deleteJsonDocument', path: options.path, id: options.id });
if (!this.deleteJsonDocumentUseCase) {
throw DomainErrors.featureNotAvailable('JSON document features');
}
const result = await this.deleteJsonDocumentUseCase.execute({
branchName: undefined, // Global memory bank
path: options.path,
id: options.id,
});
return this.presenter.presentSuccess(result); // Already correct, no change needed here
}
catch (error) {
return this.handleError(error, 'deleteJsonDocument');
}
}
/**
* List JSON documents in global memory bank
* @param options Options for listing documents (type, tags)
* @returns Promise resolving to MCP response with list of documents
*/
async listJsonDocuments(options) {
try {
this.componentLogger.info('Listing global JSON documents', { operation: 'listJsonDocuments', type: options?.type, tags: options?.tags });
if (!this.searchJsonDocumentsUseCase) {
throw DomainErrors.featureNotAvailable('JSON document features');
}
const result = await this.searchJsonDocumentsUseCase.execute({
branchName: undefined, // Global memory bank
documentType: options?.type,
tags: options?.tags,
});
return this.presenter.presentSuccess(result.documents); // Already correct, no change needed here
}
catch (error) {
return this.handleError(error, 'listJsonDocuments');
}
}
/**
* Search JSON documents in global memory bank
* @param query Search query
* @returns Promise resolving to MCP response with matching documents
*/
async searchJsonDocuments(query) {
try {
this.componentLogger.info(`Searching global JSON documents`, { operation: 'searchJsonDocuments', query });
if (!this.searchJsonDocumentsUseCase) {
throw DomainErrors.featureNotAvailable('JSON document features');
}
const result = await this.searchJsonDocumentsUseCase.execute({
branchName: undefined, // Global memory bank
});
return this.presenter.presentSuccess(result.documents); // Already correct, no change needed here
}
catch (error) {
return this.handleError(error, 'searchJsonDocuments');
}
}
/**
* Update JSON index in global memory bank
* @param options Options for updating index (force rebuild)
* @returns Promise resolving to MCP response with the result
*/
async updateJsonIndex(options) {
try {
this.componentLogger.info(`Updating global JSON index`, { operation: 'updateJsonIndex', force: options?.force });
if (!this.updateJsonIndexUseCase) {
throw DomainErrors.featureNotAvailable('JSON document features');
}
const result = await this.updateJsonIndexUseCase.execute({
branchName: undefined, // Global memory bank
fullRebuild: options?.force,
});
return this.presenter.presentSuccess(result); // Already correct, no change needed here
}
catch (error) {
return this.handleError(error, 'updateJsonIndex');
}
}
}
//# sourceMappingURL=GlobalController.js.map