# Directory Structure ``` ├── .DS_Store ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ └── feature_request.md │ └── pull_request_template.md ├── .gitignore ├── .npmrc ├── custom-instructions.md ├── Dockerfile ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── smithery.yaml ├── src │ ├── data │ │ ├── protocols │ │ │ ├── file-repository.ts │ │ │ ├── index.ts │ │ │ └── project-repository.ts │ │ └── usecases │ │ ├── list-project-files │ │ │ ├── list-project-files-protocols.ts │ │ │ └── list-project-files.ts │ │ ├── list-projects │ │ │ ├── list-projects-protocols.ts │ │ │ └── list-projects.ts │ │ ├── read-file │ │ │ ├── read-file-protocols.ts │ │ │ └── read-file.ts │ │ ├── update-file │ │ │ ├── update-file-protocols.ts │ │ │ └── update-file.ts │ │ └── write-file │ │ ├── write-file-protocols.ts │ │ └── write-file.ts │ ├── domain │ │ ├── entities │ │ │ ├── file.ts │ │ │ ├── index.ts │ │ │ └── project.ts │ │ └── usecases │ │ ├── index.ts │ │ ├── list-project-files.ts │ │ ├── list-projects.ts │ │ ├── read-file.ts │ │ ├── update-file.ts │ │ └── write-file.ts │ ├── infra │ │ └── filesystem │ │ ├── index.ts │ │ └── repositories │ │ ├── fs-file-repository.ts │ │ └── fs-project-repository.ts │ ├── main │ │ ├── config │ │ │ └── env.ts │ │ ├── factories │ │ │ ├── controllers │ │ │ │ ├── index.ts │ │ │ │ ├── list-project-files │ │ │ │ │ ├── list-project-files-controller-factory.ts │ │ │ │ │ └── list-project-files-validation-factory.ts │ │ │ │ ├── list-projects │ │ │ │ │ └── list-projects-controller-factory.ts │ │ │ │ ├── read │ │ │ │ │ ├── read-controller-factory.ts │ │ │ │ │ └── read-validation-factory.ts │ │ │ │ ├── update │ │ │ │ │ ├── update-controller-factory.ts │ │ │ │ │ └── update-validation-factory.ts │ │ │ │ └── write │ │ │ │ ├── write-controller-factory.ts │ │ │ │ └── write-validation-factory.ts │ │ │ └── use-cases │ │ │ ├── index.ts │ │ │ ├── list-project-files-factory.ts │ │ │ ├── list-projects-factory.ts │ │ │ ├── read-file-factory.ts │ │ │ ├── update-file-factory.ts │ │ │ └── write-file-factory.ts │ │ ├── index.ts │ │ └── protocols │ │ └── mcp │ │ ├── adapters │ │ │ ├── mcp-request-adapter.ts │ │ │ ├── mcp-router-adapter.ts │ │ │ └── mcp-server-adapter.ts │ │ ├── app.ts │ │ ├── helpers │ │ │ └── serialize-error.ts │ │ └── routes.ts │ ├── presentation │ │ ├── controllers │ │ │ ├── index.ts │ │ │ ├── list-project-files │ │ │ │ ├── index.ts │ │ │ │ ├── list-project-files-controller.ts │ │ │ │ └── protocols.ts │ │ │ ├── list-projects │ │ │ │ ├── index.ts │ │ │ │ ├── list-projects-controller.ts │ │ │ │ └── protocols.ts │ │ │ ├── read │ │ │ │ ├── index.ts │ │ │ │ ├── protocols.ts │ │ │ │ └── read-controller.ts │ │ │ ├── update │ │ │ │ ├── index.ts │ │ │ │ ├── protocols.ts │ │ │ │ └── update-controller.ts │ │ │ └── write │ │ │ ├── index.ts │ │ │ ├── protocols.ts │ │ │ └── write-controller.ts │ │ ├── errors │ │ │ ├── base-error.ts │ │ │ ├── error-names.ts │ │ │ ├── index.ts │ │ │ ├── invalid-param-error.ts │ │ │ ├── missing-param-error.ts │ │ │ ├── not-found-error.ts │ │ │ └── unexpected-error.ts │ │ ├── helpers │ │ │ └── index.ts │ │ └── protocols │ │ ├── controller.ts │ │ ├── index.ts │ │ ├── request.ts │ │ ├── response.ts │ │ └── validator.ts │ └── validators │ ├── constants.ts │ ├── index.ts │ ├── param-name-validator.ts │ ├── path-security-validator.ts │ ├── required-field-validator.ts │ └── validator-composite.ts ├── tests │ ├── data │ │ ├── mocks │ │ │ ├── index.ts │ │ │ ├── mock-file-repository.ts │ │ │ └── mock-project-repository.ts │ │ └── usecases │ │ ├── list-project-files │ │ │ └── list-project-files.spec.ts │ │ ├── list-projects │ │ │ └── list-projects.spec.ts │ │ ├── read-file │ │ │ └── read-file.spec.ts │ │ ├── update-file │ │ │ └── update-file.spec.ts │ │ └── write-file │ │ └── write-file.spec.ts │ ├── infra │ │ └── filesystem │ │ └── repositories │ │ ├── fs-file-repository.test.ts │ │ └── fs-project-repository.test.ts │ ├── presentation │ │ ├── controllers │ │ │ ├── list-project-files │ │ │ │ └── list-project-files-controller.test.ts │ │ │ ├── list-projects │ │ │ │ └── list-projects-controller.test.ts │ │ │ ├── read │ │ │ │ └── read-controller.test.ts │ │ │ ├── update │ │ │ │ └── update-controller.test.ts │ │ │ └── write │ │ │ └── write-controller.test.ts │ │ ├── helpers │ │ │ └── http-helpers.test.ts │ │ └── mocks │ │ ├── index.ts │ │ ├── mock-list-project-files-use-case.ts │ │ ├── mock-list-projects-use-case.ts │ │ ├── mock-read-file-use-case.ts │ │ ├── mock-update-file-use-case.ts │ │ ├── mock-validator.ts │ │ └── mock-write-file-use-case.ts │ └── validators │ ├── param-name-validator.test.ts │ ├── path-security-validator.test.ts │ ├── required-field-validator.test.ts │ └── validator-composite.test.ts ├── tsconfig.json └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules 2 | .DS_Store 3 | .env 4 | .env.local 5 | .env.development.local 6 | .env.test.local 7 | .env.production.local 8 | dist 9 | coverage ``` -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- ``` 1 | # Use package-lock.json for dependency tracking 2 | package-lock=true 3 | 4 | # Save exact versions for better reproducibility 5 | save-exact=true 6 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Memory Bank MCP Server 2 | 3 | [](https://smithery.ai/server/@alioshr/memory-bank-mcp) 4 | [](https://www.npmjs.com/package/@allpepper/memory-bank-mcp) 5 | [](https://www.npmjs.com/package/@allpepper/memory-bank-mcp) 6 | 7 | <a href="https://glama.ai/mcp/servers/ir18x1tixp"><img width="380" height="200" src="https://glama.ai/mcp/servers/ir18x1tixp/badge" alt="Memory Bank Server MCP server" /></a> 8 | 9 | A Model Context Protocol (MCP) server implementation for remote memory bank management, inspired by [Cline Memory Bank](https://github.com/nickbaumann98/cline_docs/blob/main/prompting/custom%20instructions%20library/cline-memory-bank.md). 10 | 11 | ## Overview 12 | 13 | The Memory Bank MCP Server transforms traditional file-based memory banks into a centralized service that: 14 | 15 | - Provides remote access to memory bank files via MCP protocol 16 | - Enables multi-project memory bank management 17 | - Maintains consistent file structure and validation 18 | - Ensures proper isolation between project memory banks 19 | 20 | ## Features 21 | 22 | - **Multi-Project Support** 23 | 24 | - Project-specific directories 25 | - File structure enforcement 26 | - Path traversal prevention 27 | - Project listing capabilities 28 | - File listing per project 29 | 30 | - **Remote Accessibility** 31 | 32 | - Full MCP protocol implementation 33 | - Type-safe operations 34 | - Proper error handling 35 | - Security through project isolation 36 | 37 | - **Core Operations** 38 | - Read/write/update memory bank files 39 | - List available projects 40 | - List files within projects 41 | - Project existence validation 42 | - Safe read-only operations 43 | 44 | ## Installation 45 | 46 | To install Memory Bank Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@alioshr/memory-bank-mcp): 47 | 48 | ```bash 49 | npx -y @smithery/cli install @alioshr/memory-bank-mcp --client claude 50 | ``` 51 | 52 | This will set up the MCP server configuration automatically. Alternatively, you can configure the server manually as described in the Configuration section below. 53 | 54 | ## Quick Start 55 | 56 | 1. Configure the MCP server in your settings (see Configuration section below) 57 | 2. Start using the memory bank tools in your AI assistant 58 | 59 | ## Using with Cline/Roo Code 60 | 61 | The memory bank MCP server needs to be configured in your Cline MCP settings file. The location depends on your setup: 62 | 63 | - For Cline extension: `~/Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` 64 | - For Roo Code VS Code extension: `~/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json` 65 | 66 | Add the following configuration to your MCP settings: 67 | 68 | ```json 69 | { 70 | "allpepper-memory-bank": { 71 | "command": "npx", 72 | "args": ["-y", "@allpepper/memory-bank-mcp"], 73 | "env": { 74 | "MEMORY_BANK_ROOT": "<path-to-bank>" 75 | }, 76 | "disabled": false, 77 | "autoApprove": [ 78 | "memory_bank_read", 79 | "memory_bank_write", 80 | "memory_bank_update", 81 | "list_projects", 82 | "list_project_files" 83 | ] 84 | } 85 | } 86 | ``` 87 | 88 | ### Configuration Details 89 | 90 | - `MEMORY_BANK_ROOT`: Directory where project memory banks will be stored (e.g., `/path/to/memory-bank`) 91 | - `disabled`: Set to `false` to enable the server 92 | - `autoApprove`: List of operations that don't require explicit user approval: 93 | - `memory_bank_read`: Read memory bank files 94 | - `memory_bank_write`: Create new memory bank files 95 | - `memory_bank_update`: Update existing memory bank files 96 | - `list_projects`: List available projects 97 | - `list_project_files`: List files within a project 98 | 99 | ## Using with Cursor 100 | 101 | For Cursor, open the settings -> features -> add MCP server -> add the following: 102 | 103 | ```shell 104 | env MEMORY_BANK_ROOT=<path-to-bank> npx -y @allpepper/memory-bank-mcp@latest 105 | ``` 106 | ## Using with Claude 107 | 108 | - Claude desktop config file: `~/Library/Application Support/Claude/claude_desktop_config.json` 109 | - Claude Code config file: `~/.claude.json` 110 | 111 | 1. Locate the config file 112 | 3. Locate the property called `mcpServers` 113 | 4. Paste this: 114 | 115 | ``` 116 | "allPepper-memory-bank": { 117 | "type": "stdio", 118 | "command": "npx", 119 | "args": [ 120 | "-y", 121 | "@allpepper/memory-bank-mcp@latest" 122 | ], 123 | "env": { 124 | "MEMORY_BANK_ROOT": "YOUR PATH" 125 | } 126 | } 127 | ``` 128 | 129 | ## Custom AI instructions 130 | 131 | This section contains the instructions that should be pasted on the AI custom instructions, either for Cline, Claude or Cursor, or any other MCP client. You should copy and paste these rules. For reference, see [custom-instructions.md](custom-instructions.md) which contains these rules. 132 | 133 | ## Development 134 | 135 | Basic development commands: 136 | 137 | ```bash 138 | # Install dependencies 139 | npm install 140 | 141 | # Build the project 142 | npm run build 143 | 144 | # Run tests 145 | npm run test 146 | 147 | # Run tests in watch mode 148 | npm run test:watch 149 | 150 | # Run the server directly with ts-node for quick testing 151 | npm run dev 152 | ``` 153 | 154 | ### Running with Docker 155 | 156 | 1. Build the Docker image: 157 | 158 | ```bash 159 | docker build -t memory-bank-mcp:local . 160 | ``` 161 | 162 | 2. Run the Docker container for testing: 163 | 164 | ```bash 165 | docker run -i --rm \ 166 | -e MEMORY_BANK_ROOT="/mnt/memory_bank" \ 167 | -v /path/to/memory-bank:/mnt/memory_bank \ 168 | --entrypoint /bin/sh \ 169 | memory-bank-mcp:local \ 170 | -c "ls -la /mnt/memory_bank" 171 | ``` 172 | 173 | 3. Add MCP configuration, example for Roo Code: 174 | 175 | ```json 176 | "allpepper-memory-bank": { 177 | "command": "docker", 178 | "args": [ 179 | "run", "-i", "--rm", 180 | "-e", 181 | "MEMORY_BANK_ROOT", 182 | "-v", 183 | "/path/to/memory-bank:/mnt/memory_bank", 184 | "memory-bank-mcp:local" 185 | ], 186 | "env": { 187 | "MEMORY_BANK_ROOT": "/mnt/memory_bank" 188 | }, 189 | "disabled": false, 190 | "alwaysAllow": [ 191 | "list_projects", 192 | "list_project_files", 193 | "memory_bank_read", 194 | "memory_bank_update", 195 | "memory_bank_write" 196 | ] 197 | } 198 | ``` 199 | 200 | ## Contributing 201 | 202 | Contributions are welcome! Please follow these steps: 203 | 204 | 1. Fork the repository 205 | 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 206 | 3. Commit your changes (`git commit -m 'Add amazing feature'`) 207 | 4. Push to the branch (`git push origin feature/amazing-feature`) 208 | 5. Open a Pull Request 209 | 210 | ### Development Guidelines 211 | 212 | - Use TypeScript for all new code 213 | - Maintain type safety across the codebase 214 | - Add tests for new features 215 | - Update documentation as needed 216 | - Follow existing code style and patterns 217 | 218 | ### Testing 219 | 220 | - Write unit tests for new features 221 | - Include multi-project scenario tests 222 | - Test error cases thoroughly 223 | - Validate type constraints 224 | - Mock filesystem operations appropriately 225 | 226 | ## License 227 | 228 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 229 | 230 | ## Acknowledgments 231 | 232 | This project implements the memory bank concept originally documented in the [Cline Memory Bank](https://github.com/nickbaumann98/cline_docs/blob/main/prompting/custom%20instructions%20library/cline-memory-bank.md), extending it with remote capabilities and multi-project support. 233 | ``` -------------------------------------------------------------------------------- /src/domain/entities/file.ts: -------------------------------------------------------------------------------- ```typescript 1 | export type File = string; 2 | ``` -------------------------------------------------------------------------------- /src/domain/entities/project.ts: -------------------------------------------------------------------------------- ```typescript 1 | export type Project = string; 2 | ``` -------------------------------------------------------------------------------- /src/presentation/protocols/request.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface Request<T extends any> { 2 | body?: T; 3 | } 4 | ``` -------------------------------------------------------------------------------- /src/domain/entities/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./file.js"; 2 | export * from "./project.js"; 3 | ``` -------------------------------------------------------------------------------- /src/main/config/env.ts: -------------------------------------------------------------------------------- ```typescript 1 | export const env = { 2 | rootPath: process.env.MEMORY_BANK_ROOT!, 3 | }; 4 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/read/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./protocols.js"; 2 | export * from "./read-controller.js"; 3 | ``` -------------------------------------------------------------------------------- /src/presentation/protocols/validator.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface Validator { 2 | validate(input?: any): Error | null; 3 | } 4 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/write/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./protocols.js"; 2 | export * from "./write-controller.js"; 3 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/update/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./protocols.js"; 2 | export * from "./update-controller.js"; 3 | ``` -------------------------------------------------------------------------------- /src/data/protocols/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./file-repository.js"; 2 | export * from "./project-repository.js"; 3 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/list-projects/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./list-projects-controller.js"; 2 | export * from "./protocols.js"; 3 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/list-project-files/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./list-project-files-controller.js"; 2 | export * from "./protocols.js"; 3 | ``` -------------------------------------------------------------------------------- /tests/data/mocks/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./mock-file-repository.js"; 2 | export * from "./mock-project-repository.js"; 3 | ``` -------------------------------------------------------------------------------- /src/infra/filesystem/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./repositories/fs-file-repository.js"; 2 | export * from "./repositories/fs-project-repository.js"; 3 | ``` -------------------------------------------------------------------------------- /src/presentation/protocols/response.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface Response<T extends any | Error | null = any | Error | null> { 2 | body?: T; 3 | statusCode: number; 4 | } 5 | ``` -------------------------------------------------------------------------------- /src/presentation/protocols/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./controller.js"; 2 | export * from "./request.js"; 3 | export * from "./response.js"; 4 | export * from "./validator.js"; 5 | ``` -------------------------------------------------------------------------------- /src/domain/usecases/list-projects.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Project } from "../entities/index.js"; 2 | 3 | export interface ListProjectsUseCase { 4 | listProjects(): Promise<Project[]>; 5 | } 6 | ``` -------------------------------------------------------------------------------- /src/validators/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./param-name-validator.js"; 2 | export * from "./required-field-validator.js"; 3 | export * from "./validator-composite.js"; 4 | ``` -------------------------------------------------------------------------------- /src/presentation/protocols/controller.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Request, Response } from "./index.js"; 2 | 3 | export interface Controller<T, R> { 4 | handle(request: Request<T>): Promise<Response<R>>; 5 | } 6 | ``` -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import app from "./protocols/mcp/app.js"; 4 | 5 | app.start().catch((error) => { 6 | console.error(error); 7 | process.exit(1); 8 | }); 9 | ``` -------------------------------------------------------------------------------- /src/domain/usecases/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./list-project-files.js"; 2 | export * from "./list-projects.js"; 3 | export * from "./read-file.js"; 4 | export * from "./update-file.js"; 5 | export * from "./write-file.js"; 6 | ``` -------------------------------------------------------------------------------- /src/validators/constants.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Regular expression for validating project and file names 3 | * Allows only alphanumeric characters, underscores, and hyphens 4 | */ 5 | export const NAME_REGEX = /^[a-zA-Z0-9_.-]+$/; 6 | ``` -------------------------------------------------------------------------------- /src/presentation/errors/base-error.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorName } from "./error-names.js"; 2 | 3 | export abstract class BaseError extends Error { 4 | constructor(message: string, name: ErrorName) { 5 | super(message); 6 | this.name = name; 7 | } 8 | } 9 | ``` -------------------------------------------------------------------------------- /src/presentation/errors/error-names.ts: -------------------------------------------------------------------------------- ```typescript 1 | export enum ErrorName { 2 | INVALID_PARAM_ERROR = "InvalidParamError", 3 | NOT_FOUND_ERROR = "NotFoundError", 4 | UNEXPECTED_ERROR = "UnexpectedError", 5 | MISSING_PARAM_ERROR = "MissingParamError", 6 | } 7 | ``` -------------------------------------------------------------------------------- /src/main/factories/use-cases/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./list-project-files-factory.js"; 2 | export * from "./list-projects-factory.js"; 3 | export * from "./read-file-factory.js"; 4 | export * from "./update-file-factory.js"; 5 | export * from "./write-file-factory.js"; 6 | ``` -------------------------------------------------------------------------------- /src/domain/usecases/read-file.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { File } from "../entities/index.js"; 2 | export interface ReadFileParams { 3 | projectName: string; 4 | fileName: string; 5 | } 6 | 7 | export interface ReadFileUseCase { 8 | readFile(params: ReadFileParams): Promise<File | null>; 9 | } 10 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Export all controller modules 2 | export * from "./list-project-files/index.js"; 3 | export * from "./list-projects/index.js"; 4 | export * from "./read/index.js"; 5 | export * from "./update/index.js"; 6 | export * from "./write/index.js"; 7 | ``` -------------------------------------------------------------------------------- /src/domain/usecases/list-project-files.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { File } from "../entities/index.js"; 2 | export interface ListProjectFilesParams { 3 | projectName: string; 4 | } 5 | 6 | export interface ListProjectFilesUseCase { 7 | listProjectFiles(params: ListProjectFilesParams): Promise<File[]>; 8 | } 9 | ``` -------------------------------------------------------------------------------- /src/presentation/errors/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./base-error.js"; 2 | export * from "./error-names.js"; 3 | export * from "./invalid-param-error.js"; 4 | export * from "./missing-param-error.js"; 5 | export * from "./not-found-error.js"; 6 | export * from "./unexpected-error.js"; 7 | ``` -------------------------------------------------------------------------------- /src/data/protocols/project-repository.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Project } from "../../domain/entities/index.js"; 2 | 3 | export interface ProjectRepository { 4 | listProjects(): Promise<Project[]>; 5 | projectExists(name: string): Promise<boolean>; 6 | ensureProject(name: string): Promise<void>; 7 | } 8 | ``` -------------------------------------------------------------------------------- /src/domain/usecases/write-file.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { File } from "../entities/index.js"; 2 | export interface WriteFileParams { 3 | projectName: string; 4 | fileName: string; 5 | content: string; 6 | } 7 | 8 | export interface WriteFileUseCase { 9 | writeFile(params: WriteFileParams): Promise<File | null>; 10 | } 11 | ``` -------------------------------------------------------------------------------- /src/presentation/errors/not-found-error.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseError } from "./base-error.js"; 2 | import { ErrorName } from "./error-names.js"; 3 | 4 | export class NotFoundError extends BaseError { 5 | constructor(name: string) { 6 | super(`Resource not found: ${name}`, ErrorName.NOT_FOUND_ERROR); 7 | } 8 | } 9 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/list-projects/protocols.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ListProjectsUseCase } from "../../../domain/usecases/list-projects.js"; 2 | import { Controller, Response } from "../../protocols/index.js"; 3 | 4 | export type ListProjectsResponse = string[]; 5 | 6 | export { Controller, ListProjectsUseCase, Response }; 7 | ``` -------------------------------------------------------------------------------- /src/data/usecases/read-file/read-file-protocols.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | ReadFileParams, 3 | ReadFileUseCase, 4 | } from "../../../domain/usecases/index.js"; 5 | import { FileRepository, ProjectRepository } from "../../protocols/index.js"; 6 | 7 | export { FileRepository, ProjectRepository, ReadFileParams, ReadFileUseCase }; 8 | ``` -------------------------------------------------------------------------------- /src/domain/usecases/update-file.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { File } from "../entities/index.js"; 2 | 3 | export interface UpdateFileParams { 4 | projectName: string; 5 | fileName: string; 6 | content: string; 7 | } 8 | 9 | export interface UpdateFileUseCase { 10 | updateFile(params: UpdateFileParams): Promise<File | null>; 11 | } 12 | ``` -------------------------------------------------------------------------------- /src/data/usecases/write-file/write-file-protocols.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | WriteFileParams, 3 | WriteFileUseCase, 4 | } from "../../../domain/usecases/index.js"; 5 | import { FileRepository, ProjectRepository } from "../../protocols/index.js"; 6 | 7 | export { FileRepository, ProjectRepository, WriteFileParams, WriteFileUseCase }; 8 | ``` -------------------------------------------------------------------------------- /src/main/protocols/mcp/app.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServerAdapter } from "./adapters/mcp-server-adapter.js"; 2 | import routes from "./routes.js"; 3 | 4 | const router = routes(); 5 | const app = new McpServerAdapter(router); 6 | 7 | app.register({ 8 | name: "memory-bank", 9 | version: "1.0.0", 10 | }); 11 | 12 | export default app; 13 | ``` -------------------------------------------------------------------------------- /src/data/usecases/list-projects/list-projects-protocols.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Project } from "../../../domain/entities/index.js"; 2 | import { ListProjectsUseCase } from "../../../domain/usecases/index.js"; 3 | import { ProjectRepository } from "../../protocols/index.js"; 4 | 5 | export { ListProjectsUseCase, Project, ProjectRepository }; 6 | ``` -------------------------------------------------------------------------------- /src/presentation/errors/invalid-param-error.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseError } from "./base-error.js"; 2 | import { ErrorName } from "./error-names.js"; 3 | 4 | export class InvalidParamError extends BaseError { 5 | constructor(paramName: string) { 6 | super(`Invalid parameter: ${paramName}`, ErrorName.INVALID_PARAM_ERROR); 7 | } 8 | } 9 | ``` -------------------------------------------------------------------------------- /src/presentation/errors/missing-param-error.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseError } from "./base-error.js"; 2 | import { ErrorName } from "./error-names.js"; 3 | 4 | export class MissingParamError extends BaseError { 5 | constructor(paramName: string) { 6 | super(`Missing parameter: ${paramName}`, ErrorName.MISSING_PARAM_ERROR); 7 | } 8 | } 9 | ``` -------------------------------------------------------------------------------- /src/data/usecases/update-file/update-file-protocols.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | UpdateFileParams, 3 | UpdateFileUseCase, 4 | } from "../../../domain/usecases/index.js"; 5 | import { FileRepository, ProjectRepository } from "../../protocols/index.js"; 6 | 7 | export { 8 | FileRepository, 9 | ProjectRepository, 10 | UpdateFileParams, 11 | UpdateFileUseCase, 12 | }; 13 | ``` -------------------------------------------------------------------------------- /tests/presentation/mocks/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./mock-list-project-files-use-case.js"; 2 | export * from "./mock-list-projects-use-case.js"; 3 | export * from "./mock-read-file-use-case.js"; 4 | export * from "./mock-update-file-use-case.js"; 5 | export * from "./mock-validator.js"; 6 | export * from "./mock-write-file-use-case.js"; 7 | ``` -------------------------------------------------------------------------------- /src/data/usecases/list-project-files/list-project-files-protocols.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | ListProjectFilesParams, 3 | ListProjectFilesUseCase, 4 | } from "../../../domain/usecases/index.js"; 5 | import { FileRepository, ProjectRepository } from "../../protocols/index.js"; 6 | 7 | export { 8 | FileRepository, 9 | ListProjectFilesParams, 10 | ListProjectFilesUseCase, 11 | ProjectRepository, 12 | }; 13 | ``` -------------------------------------------------------------------------------- /src/presentation/errors/unexpected-error.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseError } from "./base-error.js"; 2 | import { ErrorName } from "./error-names.js"; 3 | 4 | export class UnexpectedError extends BaseError { 5 | constructor(originalError: unknown) { 6 | super( 7 | `An unexpected error occurred: ${originalError}`, 8 | ErrorName.UNEXPECTED_ERROR 9 | ); 10 | } 11 | } 12 | ``` -------------------------------------------------------------------------------- /src/main/factories/controllers/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./list-project-files/list-project-files-controller-factory.js"; 2 | export * from "./list-projects/list-projects-controller-factory.js"; 3 | export * from "./read/read-controller-factory.js"; 4 | export * from "./update/update-controller-factory.js"; 5 | export * from "./write/write-controller-factory.js"; 6 | ``` -------------------------------------------------------------------------------- /src/data/usecases/list-projects/list-projects.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | ListProjectsUseCase, 3 | Project, 4 | ProjectRepository, 5 | } from "./list-projects-protocols.js"; 6 | 7 | export class ListProjects implements ListProjectsUseCase { 8 | constructor(private readonly projectRepository: ProjectRepository) {} 9 | 10 | async listProjects(): Promise<Project[]> { 11 | return this.projectRepository.listProjects(); 12 | } 13 | } 14 | ``` -------------------------------------------------------------------------------- /tests/presentation/mocks/mock-list-projects-use-case.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ListProjectsUseCase } from "../../../src/domain/usecases/list-projects.js"; 2 | 3 | export class MockListProjectsUseCase implements ListProjectsUseCase { 4 | async listProjects(): Promise<string[]> { 5 | return ["project1", "project2"]; 6 | } 7 | } 8 | 9 | export const makeListProjectsUseCase = (): ListProjectsUseCase => { 10 | return new MockListProjectsUseCase(); 11 | }; 12 | ``` -------------------------------------------------------------------------------- /tests/presentation/mocks/mock-validator.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Validator } from "../../../src/presentation/protocols/index.js"; 2 | 3 | export class MockValidator<T> implements Validator<T> { 4 | validate<S extends T>(input: S): null; 5 | validate(input?: any): Error; 6 | validate(input?: any): Error | null { 7 | return null; 8 | } 9 | } 10 | 11 | export const makeValidator = <T>(): Validator<T> => { 12 | return new MockValidator<T>(); 13 | }; 14 | ``` -------------------------------------------------------------------------------- /src/main/factories/controllers/list-projects/list-projects-controller-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ListProjectsController } from "../../../../presentation/controllers/list-projects/list-projects-controller.js"; 2 | import { makeListProjects } from "../../use-cases/list-projects-factory.js"; 3 | 4 | export const makeListProjectsController = () => { 5 | const listProjectsUseCase = makeListProjects(); 6 | return new ListProjectsController(listProjectsUseCase); 7 | }; 8 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/write/protocols.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WriteFileUseCase } from "../../../domain/usecases/write-file.js"; 2 | import { 3 | Controller, 4 | Request, 5 | Response, 6 | Validator, 7 | } from "../../protocols/index.js"; 8 | 9 | export interface WriteRequest { 10 | projectName: string; 11 | fileName: string; 12 | content: string; 13 | } 14 | 15 | export type WriteResponse = string; 16 | 17 | export { Controller, Request, Response, Validator, WriteFileUseCase }; 18 | ``` -------------------------------------------------------------------------------- /src/main/factories/controllers/list-project-files/list-project-files-validation-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Validator } from "../../../../presentation/protocols/validator.js"; 2 | import { ValidatorComposite } from "../../../../validators/validator-composite.js"; 3 | 4 | const makeValidations = (): Validator[] => { 5 | return []; 6 | }; 7 | 8 | export const makeListProjectFilesValidation = (): Validator => { 9 | const validations = makeValidations(); 10 | return new ValidatorComposite(validations); 11 | }; 12 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/list-project-files/protocols.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ListProjectFilesUseCase } from "../../../domain/usecases/list-project-files.js"; 2 | import { 3 | Controller, 4 | Request, 5 | Response, 6 | Validator, 7 | } from "../../protocols/index.js"; 8 | 9 | export interface ListProjectFilesRequest { 10 | projectName: string; 11 | } 12 | 13 | export type ListProjectFilesResponse = string[]; 14 | 15 | export { Controller, ListProjectFilesUseCase, Request, Response, Validator }; 16 | ``` -------------------------------------------------------------------------------- /src/main/factories/use-cases/list-projects-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ListProjects } from "../../../data/usecases/list-projects/list-projects.js"; 2 | import { FsProjectRepository } from "../../../infra/filesystem/repositories/fs-project-repository.js"; 3 | import { env } from "../../config/env.js"; 4 | 5 | export const makeListProjects = () => { 6 | const projectRepository = new FsProjectRepository(env.rootPath); 7 | return new ListProjects(projectRepository); 8 | }; 9 | ``` -------------------------------------------------------------------------------- /src/validators/validator-composite.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Validator } from "../presentation/protocols/validator.js"; 2 | 3 | export class ValidatorComposite implements Validator { 4 | constructor(private readonly validators: Array<Validator>) {} 5 | 6 | validate(input?: any): Error | null { 7 | for (const validator of this.validators) { 8 | const error = validator.validate(input); 9 | if (error) { 10 | return error; 11 | } 12 | } 13 | return null; 14 | } 15 | } 16 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "node16", 5 | "moduleResolution": "node16", 6 | "lib": ["ES2022"], 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "resolveJsonModule": true 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 17 | } ``` -------------------------------------------------------------------------------- /src/main/factories/controllers/read/read-controller-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ReadController } from "../../../../presentation/controllers/read/read-controller.js"; 2 | import { makeReadFile } from "../../use-cases/read-file-factory.js"; 3 | import { makeReadValidation } from "./read-validation-factory.js"; 4 | 5 | export const makeReadController = () => { 6 | const validator = makeReadValidation(); 7 | const readFileUseCase = makeReadFile(); 8 | 9 | return new ReadController(readFileUseCase, validator); 10 | }; 11 | ``` -------------------------------------------------------------------------------- /tests/presentation/mocks/mock-read-file-use-case.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ReadFileUseCase } from "../../../src/domain/usecases/read-file.js"; 2 | import { ReadRequest } from "../../../src/presentation/controllers/read/protocols.js"; 3 | 4 | export class MockReadFileUseCase implements ReadFileUseCase { 5 | async readFile(params: ReadRequest): Promise<string | null> { 6 | return "file content"; 7 | } 8 | } 9 | 10 | export const makeReadFileUseCase = (): ReadFileUseCase => { 11 | return new MockReadFileUseCase(); 12 | }; 13 | ``` -------------------------------------------------------------------------------- /tests/presentation/mocks/mock-write-file-use-case.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WriteFileUseCase } from "../../../src/domain/usecases/write-file.js"; 2 | import { WriteRequest } from "../../../src/presentation/controllers/write/protocols.js"; 3 | 4 | export class MockWriteFileUseCase implements WriteFileUseCase { 5 | async writeFile(params: WriteRequest): Promise<string | null> { 6 | return null; 7 | } 8 | } 9 | 10 | export const makeWriteFileUseCase = (): WriteFileUseCase => { 11 | return new MockWriteFileUseCase(); 12 | }; 13 | ``` -------------------------------------------------------------------------------- /src/data/protocols/file-repository.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { File } from "../../domain/entities/index.js"; 2 | 3 | export interface FileRepository { 4 | listFiles(projectName: string): Promise<File[]>; 5 | loadFile(projectName: string, fileName: string): Promise<File | null>; 6 | writeFile( 7 | projectName: string, 8 | fileName: string, 9 | content: string 10 | ): Promise<File | null>; 11 | updateFile( 12 | projectName: string, 13 | fileName: string, 14 | content: string 15 | ): Promise<File | null>; 16 | } 17 | ``` -------------------------------------------------------------------------------- /src/main/factories/controllers/write/write-controller-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WriteController } from "../../../../presentation/controllers/write/write-controller.js"; 2 | import { makeWriteFile } from "../../use-cases/write-file-factory.js"; 3 | import { makeWriteValidation } from "./write-validation-factory.js"; 4 | 5 | export const makeWriteController = () => { 6 | const validator = makeWriteValidation(); 7 | const writeFileUseCase = makeWriteFile(); 8 | 9 | return new WriteController(writeFileUseCase, validator); 10 | }; 11 | ``` -------------------------------------------------------------------------------- /src/main/factories/controllers/update/update-controller-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { UpdateController } from "../../../../presentation/controllers/update/update-controller.js"; 2 | import { makeUpdateFile } from "../../use-cases/update-file-factory.js"; 3 | import { makeUpdateValidation } from "./update-validation-factory.js"; 4 | 5 | export const makeUpdateController = () => { 6 | const validator = makeUpdateValidation(); 7 | const updateFileUseCase = makeUpdateFile(); 8 | 9 | return new UpdateController(updateFileUseCase, validator); 10 | }; 11 | ``` -------------------------------------------------------------------------------- /tests/presentation/mocks/mock-update-file-use-case.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { UpdateFileUseCase } from "../../../src/domain/usecases/update-file.js"; 2 | import { UpdateRequest } from "../../../src/presentation/controllers/update/protocols.js"; 3 | 4 | export class MockUpdateFileUseCase implements UpdateFileUseCase { 5 | async updateFile(params: UpdateRequest): Promise<string | null> { 6 | return "updated content"; 7 | } 8 | } 9 | 10 | export const makeUpdateFileUseCase = (): UpdateFileUseCase => { 11 | return new MockUpdateFileUseCase(); 12 | }; 13 | ``` -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: "node", 7 | include: ["**/*.spec.ts", "**/*.test.ts"], 8 | exclude: ["**/node_modules/**", "**/dist/**"], 9 | coverage: { 10 | provider: "v8", 11 | reporter: ["text", "json", "html"], 12 | exclude: [ 13 | "**/node_modules/**", 14 | "**/dist/**", 15 | "**/*.d.ts", 16 | "**/*.spec.ts", 17 | "**/*.test.ts", 18 | ], 19 | }, 20 | }, 21 | }); 22 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/update/protocols.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { UpdateFileUseCase } from "../../../domain/usecases/update-file.js"; 2 | import { NotFoundError } from "../../errors/index.js"; 3 | import { 4 | Controller, 5 | Request, 6 | Response, 7 | Validator, 8 | } from "../../protocols/index.js"; 9 | 10 | export interface UpdateRequest { 11 | projectName: string; 12 | fileName: string; 13 | content: string; 14 | } 15 | 16 | export type UpdateResponse = string; 17 | export type RequestValidator = Validator; 18 | 19 | export { Controller, NotFoundError, Request, Response, UpdateFileUseCase }; 20 | ``` -------------------------------------------------------------------------------- /src/main/factories/use-cases/read-file-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ReadFile } from "../../../data/usecases/read-file/read-file.js"; 2 | import { FsFileRepository } from "../../../infra/filesystem/index.js"; 3 | import { FsProjectRepository } from "../../../infra/filesystem/repositories/fs-project-repository.js"; 4 | import { env } from "../../config/env.js"; 5 | 6 | export const makeReadFile = () => { 7 | const projectRepository = new FsProjectRepository(env.rootPath); 8 | const fileRepository = new FsFileRepository(env.rootPath); 9 | 10 | return new ReadFile(fileRepository, projectRepository); 11 | }; 12 | ``` -------------------------------------------------------------------------------- /src/validators/required-field-validator.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { MissingParamError } from "../presentation/errors/index.js"; 2 | import { Validator } from "../presentation/protocols/validator.js"; 3 | 4 | export class RequiredFieldValidator implements Validator { 5 | constructor(private readonly fieldName: string) {} 6 | 7 | validate(input?: any): Error | null { 8 | if ( 9 | !input || 10 | (input[this.fieldName] !== 0 && 11 | input[this.fieldName] !== false && 12 | !input[this.fieldName]) 13 | ) { 14 | return new MissingParamError(this.fieldName); 15 | } 16 | return null; 17 | } 18 | } 19 | ``` -------------------------------------------------------------------------------- /src/main/factories/use-cases/write-file-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WriteFile } from "../../../data/usecases/write-file/write-file.js"; 2 | import { FsFileRepository } from "../../../infra/filesystem/index.js"; 3 | import { FsProjectRepository } from "../../../infra/filesystem/repositories/fs-project-repository.js"; 4 | import { env } from "../../config/env.js"; 5 | 6 | export const makeWriteFile = () => { 7 | const projectRepository = new FsProjectRepository(env.rootPath); 8 | const fileRepository = new FsFileRepository(env.rootPath); 9 | 10 | return new WriteFile(fileRepository, projectRepository); 11 | }; 12 | ``` -------------------------------------------------------------------------------- /src/main/factories/use-cases/update-file-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { UpdateFile } from "../../../data/usecases/update-file/update-file.js"; 2 | import { FsFileRepository } from "../../../infra/filesystem/index.js"; 3 | import { FsProjectRepository } from "../../../infra/filesystem/repositories/fs-project-repository.js"; 4 | import { env } from "../../config/env.js"; 5 | 6 | export const makeUpdateFile = () => { 7 | const projectRepository = new FsProjectRepository(env.rootPath); 8 | const fileRepository = new FsFileRepository(env.rootPath); 9 | 10 | return new UpdateFile(fileRepository, projectRepository); 11 | }; 12 | ``` -------------------------------------------------------------------------------- /tests/presentation/mocks/mock-list-project-files-use-case.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ListProjectFilesUseCase } from "../../../src/domain/usecases/list-project-files.js"; 2 | import { ListProjectFilesRequest } from "../../../src/presentation/controllers/list-project-files/protocols.js"; 3 | 4 | export class MockListProjectFilesUseCase implements ListProjectFilesUseCase { 5 | async listProjectFiles(params: ListProjectFilesRequest): Promise<string[]> { 6 | return ["file1.txt", "file2.txt"]; 7 | } 8 | } 9 | 10 | export const makeListProjectFilesUseCase = (): ListProjectFilesUseCase => { 11 | return new MockListProjectFilesUseCase(); 12 | }; 13 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/read/protocols.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ReadFileUseCase } from "../../../domain/usecases/read-file.js"; 2 | import { NotFoundError } from "../../errors/index.js"; 3 | import { 4 | Controller, 5 | Request, 6 | Response, 7 | Validator, 8 | } from "../../protocols/index.js"; 9 | export interface ReadRequest { 10 | /** 11 | * The name of the project containing the file. 12 | */ 13 | projectName: string; 14 | 15 | /** 16 | * The name of the file to read. 17 | */ 18 | fileName: string; 19 | } 20 | 21 | export type ReadResponse = string; 22 | 23 | export { 24 | Controller, 25 | NotFoundError, 26 | ReadFileUseCase, 27 | Request, 28 | Response, 29 | Validator, 30 | }; 31 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM node:20-alpine AS builder 3 | 4 | # Copy the entire project 5 | COPY . /app 6 | WORKDIR /app 7 | 8 | # Use build cache for faster builds 9 | RUN --mount=type=cache,target=/root/.npm npm ci 10 | 11 | FROM node:20-alpine AS release 12 | 13 | COPY --from=builder /app/dist /app/dist 14 | COPY --from=builder /app/package.json /app/package.json 15 | COPY --from=builder /app/package-lock.json /app/package-lock.json 16 | 17 | WORKDIR /app 18 | 19 | RUN npm ci --ignore-scripts --omit=dev 20 | 21 | ENTRYPOINT ["node", "dist/main/index.js"] 22 | ``` -------------------------------------------------------------------------------- /src/presentation/helpers/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { NotFoundError, UnexpectedError } from "../errors/index.js"; 2 | import { type Response } from "../protocols/index.js"; 3 | 4 | export const badRequest = (error: Error): Response => ({ 5 | statusCode: 400, 6 | body: error, 7 | }); 8 | 9 | export const notFound = (resourceName: string): Response => ({ 10 | statusCode: 404, 11 | body: new NotFoundError(resourceName), 12 | }); 13 | 14 | export const serverError = (error: Error): Response => ({ 15 | statusCode: 500, 16 | body: new UnexpectedError(error), 17 | }); 18 | 19 | export const ok = (data: any): Response => ({ 20 | statusCode: 200, 21 | body: data, 22 | }); 23 | ``` -------------------------------------------------------------------------------- /src/main/factories/use-cases/list-project-files-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ListProjectFiles } from "../../../data/usecases/list-project-files/list-project-files.js"; 2 | import { FsFileRepository } from "../../../infra/filesystem/index.js"; 3 | import { FsProjectRepository } from "../../../infra/filesystem/repositories/fs-project-repository.js"; 4 | import { env } from "../../config/env.js"; 5 | 6 | export const makeListProjectFiles = () => { 7 | const projectRepository = new FsProjectRepository(env.rootPath); 8 | const fileRepository = new FsFileRepository(env.rootPath); 9 | 10 | return new ListProjectFiles(fileRepository, projectRepository); 11 | }; 12 | ``` -------------------------------------------------------------------------------- /src/main/factories/controllers/list-project-files/list-project-files-controller-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ListProjectFilesController } from "../../../../presentation/controllers/list-project-files/list-project-files-controller.js"; 2 | import { makeListProjectFiles } from "../../use-cases/list-project-files-factory.js"; 3 | import { makeListProjectFilesValidation } from "./list-project-files-validation-factory.js"; 4 | 5 | export const makeListProjectFilesController = () => { 6 | const validator = makeListProjectFilesValidation(); 7 | const listProjectFilesUseCase = makeListProjectFiles(); 8 | 9 | return new ListProjectFilesController(listProjectFilesUseCase, validator); 10 | }; 11 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/list-projects/list-projects-controller.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ok, serverError } from "../../helpers/index.js"; 2 | import { 3 | Controller, 4 | ListProjectsResponse, 5 | ListProjectsUseCase, 6 | Response, 7 | } from "./protocols.js"; 8 | 9 | export class ListProjectsController 10 | implements Controller<void, ListProjectsResponse> 11 | { 12 | constructor(private readonly listProjectsUseCase: ListProjectsUseCase) {} 13 | 14 | async handle(): Promise<Response<ListProjectsResponse>> { 15 | try { 16 | const projects = await this.listProjectsUseCase.listProjects(); 17 | return ok(projects); 18 | } catch (error) { 19 | return serverError(error as Error); 20 | } 21 | } 22 | } 23 | ``` -------------------------------------------------------------------------------- /tests/data/mocks/mock-project-repository.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ProjectRepository } from "../../../src/data/protocols/project-repository.js"; 2 | import { Project } from "../../../src/domain/entities/project.js"; 3 | 4 | export class MockProjectRepository implements ProjectRepository { 5 | private projects = ["project-1", "project-2"]; 6 | 7 | async listProjects(): Promise<Project[]> { 8 | return this.projects; 9 | } 10 | 11 | async projectExists(name: string): Promise<boolean> { 12 | return this.projects.includes(name); 13 | } 14 | 15 | async ensureProject(name: string): Promise<void> { 16 | if (!this.projects.includes(name)) { 17 | this.projects.push(name); 18 | } 19 | } 20 | } 21 | ``` -------------------------------------------------------------------------------- /src/data/usecases/read-file/read-file.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | FileRepository, 3 | ProjectRepository, 4 | ReadFileParams, 5 | ReadFileUseCase, 6 | } from "./read-file-protocols.js"; 7 | 8 | export class ReadFile implements ReadFileUseCase { 9 | constructor( 10 | private readonly fileRepository: FileRepository, 11 | private readonly projectRepository: ProjectRepository 12 | ) {} 13 | 14 | async readFile(params: ReadFileParams): Promise<string | null> { 15 | const { projectName, fileName } = params; 16 | 17 | const projectExists = await this.projectRepository.projectExists( 18 | projectName 19 | ); 20 | if (!projectExists) { 21 | return null; 22 | } 23 | 24 | return this.fileRepository.loadFile(projectName, fileName); 25 | } 26 | } 27 | ``` -------------------------------------------------------------------------------- /src/validators/path-security-validator.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { InvalidParamError } from "../presentation/errors/index.js"; 2 | import { Validator } from "../presentation/protocols/validator.js"; 3 | 4 | export class PathSecurityValidator implements Validator { 5 | constructor(private readonly fieldName: string) {} 6 | 7 | validate(input?: any): Error | null { 8 | if (!input || !input[this.fieldName]) { 9 | return null; 10 | } 11 | 12 | const value = input[this.fieldName]; 13 | if ( 14 | typeof value === "string" && 15 | (value.includes("..") || value.includes("/")) 16 | ) { 17 | return new InvalidParamError( 18 | `${this.fieldName} contains invalid path segments` 19 | ); 20 | } 21 | 22 | return null; 23 | } 24 | } 25 | ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - memoryBankRoot 10 | properties: 11 | memoryBankRoot: 12 | type: string 13 | description: The root directory for memory bank projects. 14 | commandFunction: 15 | # A function that produces the CLI command to start the MCP on stdio. 16 | |- 17 | (config) => ({command:'node',args:['dist/index.js'],env:{MEMORY_BANK_ROOT:config.memoryBankRoot}}) 18 | 19 | # Add the build section for Docker support 20 | build: 21 | dockerBuildPath: ./ 22 | ``` -------------------------------------------------------------------------------- /src/validators/param-name-validator.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { InvalidParamError } from "../presentation/errors/index.js"; 2 | import { Validator } from "../presentation/protocols/validator.js"; 3 | import { NAME_REGEX } from "./constants.js"; 4 | 5 | export class ParamNameValidator implements Validator { 6 | constructor( 7 | private readonly fieldName: string, 8 | private readonly regex: RegExp = NAME_REGEX 9 | ) {} 10 | 11 | validate(input?: any): Error | null { 12 | if (!input || !input[this.fieldName]) { 13 | return null; 14 | } 15 | 16 | const paramName = input[this.fieldName]; 17 | const isValid = this.regex.test(paramName); 18 | 19 | if (!isValid) { 20 | return new InvalidParamError(paramName); 21 | } 22 | 23 | return null; 24 | } 25 | } 26 | ``` -------------------------------------------------------------------------------- /src/main/factories/controllers/read/read-validation-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Validator } from "../../../../presentation/protocols/validator.js"; 2 | import { 3 | RequiredFieldValidator, 4 | ValidatorComposite, 5 | } from "../../../../validators/index.js"; 6 | import { PathSecurityValidator } from "../../../../validators/path-security-validator.js"; 7 | 8 | const makeValidations = (): Validator[] => { 9 | return [ 10 | new RequiredFieldValidator("projectName"), 11 | new RequiredFieldValidator("fileName"), 12 | new PathSecurityValidator("projectName"), 13 | new PathSecurityValidator("fileName"), 14 | ]; 15 | }; 16 | 17 | export const makeReadValidation = (): Validator => { 18 | const validations = makeValidations(); 19 | return new ValidatorComposite(validations); 20 | }; 21 | ``` -------------------------------------------------------------------------------- /src/data/usecases/list-project-files/list-project-files.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | FileRepository, 3 | ListProjectFilesParams, 4 | ListProjectFilesUseCase, 5 | ProjectRepository, 6 | } from "./list-project-files-protocols.js"; 7 | 8 | export class ListProjectFiles implements ListProjectFilesUseCase { 9 | constructor( 10 | private readonly fileRepository: FileRepository, 11 | private readonly projectRepository: ProjectRepository 12 | ) {} 13 | 14 | async listProjectFiles(params: ListProjectFilesParams): Promise<string[]> { 15 | const { projectName } = params; 16 | const projectExists = await this.projectRepository.projectExists( 17 | projectName 18 | ); 19 | 20 | if (!projectExists) { 21 | return []; 22 | } 23 | 24 | return this.fileRepository.listFiles(projectName); 25 | } 26 | } 27 | ``` -------------------------------------------------------------------------------- /src/main/factories/controllers/write/write-validation-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Validator } from "../../../../presentation/protocols/validator.js"; 2 | import { 3 | ParamNameValidator, 4 | RequiredFieldValidator, 5 | ValidatorComposite, 6 | } from "../../../../validators/index.js"; 7 | import { PathSecurityValidator } from "../../../../validators/path-security-validator.js"; 8 | 9 | const makeValidations = (): Validator[] => { 10 | return [ 11 | new RequiredFieldValidator("projectName"), 12 | new RequiredFieldValidator("fileName"), 13 | new RequiredFieldValidator("content"), 14 | new ParamNameValidator("projectName"), 15 | new ParamNameValidator("fileName"), 16 | new PathSecurityValidator("projectName"), 17 | new PathSecurityValidator("fileName"), 18 | ]; 19 | }; 20 | 21 | export const makeWriteValidation = (): Validator => { 22 | const validations = makeValidations(); 23 | return new ValidatorComposite(validations); 24 | }; 25 | ``` -------------------------------------------------------------------------------- /src/main/factories/controllers/update/update-validation-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Validator } from "../../../../presentation/protocols/validator.js"; 2 | import { 3 | ParamNameValidator, 4 | RequiredFieldValidator, 5 | ValidatorComposite, 6 | } from "../../../../validators/index.js"; 7 | import { PathSecurityValidator } from "../../../../validators/path-security-validator.js"; 8 | 9 | const makeValidations = (): Validator[] => { 10 | return [ 11 | new RequiredFieldValidator("projectName"), 12 | new RequiredFieldValidator("fileName"), 13 | new RequiredFieldValidator("content"), 14 | new ParamNameValidator("projectName"), 15 | new ParamNameValidator("fileName"), 16 | new PathSecurityValidator("projectName"), 17 | new PathSecurityValidator("fileName"), 18 | ]; 19 | }; 20 | 21 | export const makeUpdateValidation = (): Validator => { 22 | const validations = makeValidations(); 23 | return new ValidatorComposite(validations); 24 | }; 25 | ``` -------------------------------------------------------------------------------- /src/data/usecases/write-file/write-file.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | FileRepository, 3 | ProjectRepository, 4 | WriteFileParams, 5 | WriteFileUseCase, 6 | } from "./write-file-protocols.js"; 7 | 8 | export class WriteFile implements WriteFileUseCase { 9 | constructor( 10 | private readonly fileRepository: FileRepository, 11 | private readonly projectRepository: ProjectRepository 12 | ) {} 13 | 14 | async writeFile(params: WriteFileParams): Promise<string | null> { 15 | const { projectName, fileName, content } = params; 16 | 17 | await this.projectRepository.ensureProject(projectName); 18 | 19 | const existingFile = await this.fileRepository.loadFile( 20 | projectName, 21 | fileName 22 | ); 23 | if (existingFile !== null) { 24 | return null; 25 | } 26 | 27 | await this.fileRepository.writeFile(projectName, fileName, content); 28 | return await this.fileRepository.loadFile(projectName, fileName); 29 | } 30 | } 31 | ``` -------------------------------------------------------------------------------- /src/main/protocols/mcp/helpers/serialize-error.ts: -------------------------------------------------------------------------------- ```typescript 1 | interface SerializedError { 2 | name: string; 3 | error: string; 4 | stack?: string; 5 | cause?: string | SerializedError; 6 | code?: string | number; 7 | } 8 | 9 | export const serializeError = ( 10 | error: unknown, 11 | includeStack = false 12 | ): SerializedError => { 13 | if (error instanceof Error) { 14 | const serialized: SerializedError = { 15 | name: error.name, 16 | error: error.message, 17 | }; 18 | 19 | if (includeStack) { 20 | serialized.stack = error.stack; 21 | } 22 | 23 | if ("cause" in error && error.cause) { 24 | serialized.cause = 25 | error.cause instanceof Error 26 | ? serializeError(error.cause, includeStack) 27 | : String(error.cause); 28 | } 29 | 30 | if ( 31 | "code" in error && 32 | (typeof error.code === "string" || typeof error.code === "number") 33 | ) { 34 | serialized.code = error.code; 35 | } 36 | 37 | return serialized; 38 | } 39 | 40 | return { 41 | name: "UnknownError", 42 | error: String(error), 43 | }; 44 | }; 45 | ``` -------------------------------------------------------------------------------- /src/data/usecases/update-file/update-file.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | FileRepository, 3 | ProjectRepository, 4 | UpdateFileParams, 5 | UpdateFileUseCase, 6 | } from "./update-file-protocols.js"; 7 | 8 | export class UpdateFile implements UpdateFileUseCase { 9 | constructor( 10 | private readonly fileRepository: FileRepository, 11 | private readonly projectRepository: ProjectRepository 12 | ) {} 13 | 14 | async updateFile(params: UpdateFileParams): Promise<string | null> { 15 | const { projectName, fileName, content } = params; 16 | 17 | const projectExists = await this.projectRepository.projectExists( 18 | projectName 19 | ); 20 | if (!projectExists) { 21 | return null; 22 | } 23 | 24 | const existingFile = await this.fileRepository.loadFile( 25 | projectName, 26 | fileName 27 | ); 28 | if (existingFile === null) { 29 | return null; 30 | } 31 | 32 | await this.fileRepository.updateFile(projectName, fileName, content); 33 | return await this.fileRepository.loadFile(projectName, fileName); 34 | } 35 | } 36 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "bug" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Configure MCP server with '...' 16 | 2. Initialize project with '...' 17 | 3. Try to access '...' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Environment:** 24 | 25 | - OS: [e.g., macOS, Windows, Linux] 26 | - Node Version: [e.g., 18.x] 27 | - Package Version: [e.g., 0.1.0] 28 | - MCP Integration: [e.g., Cline extension, Claude desktop] 29 | 30 | **Memory Bank Configuration:** 31 | 32 | ```json 33 | // Your MCP server configuration 34 | { 35 | "memory-bank": { 36 | ... 37 | } 38 | } 39 | ``` 40 | 41 | **Error Output** 42 | 43 | ``` 44 | Paste any error messages or logs here 45 | ``` 46 | 47 | **Additional context** 48 | Add any other context about the problem here, such as project structure or specific memory bank files affected. 49 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/read/read-controller.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { badRequest, notFound, ok, serverError } from "../../helpers/index.js"; 2 | import { 3 | Controller, 4 | ReadFileUseCase, 5 | ReadRequest, 6 | ReadResponse, 7 | Request, 8 | Response, 9 | Validator, 10 | } from "./protocols.js"; 11 | 12 | export class ReadController implements Controller<ReadRequest, ReadResponse> { 13 | constructor( 14 | private readonly readFileUseCase: ReadFileUseCase, 15 | private readonly validator: Validator 16 | ) {} 17 | 18 | async handle(request: Request<ReadRequest>): Promise<Response<ReadResponse>> { 19 | try { 20 | const validationError = this.validator.validate(request.body); 21 | if (validationError) { 22 | return badRequest(validationError); 23 | } 24 | 25 | const { projectName, fileName } = request.body!; 26 | 27 | const content = await this.readFileUseCase.readFile({ 28 | projectName, 29 | fileName, 30 | }); 31 | 32 | if (content === null) { 33 | return notFound(fileName); 34 | } 35 | 36 | return ok(content); 37 | } catch (error) { 38 | return serverError(error as Error); 39 | } 40 | } 41 | } 42 | ``` -------------------------------------------------------------------------------- /src/main/protocols/mcp/adapters/mcp-request-adapter.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | Request as MCPRequest, 3 | ServerResult as MCPResponse, 4 | } from "@modelcontextprotocol/sdk/types.js"; 5 | import { Controller } from "../../../../presentation/protocols/controller.js"; 6 | import { serializeError } from "../helpers/serialize-error.js"; 7 | import { MCPRequestHandler } from "./mcp-router-adapter.js"; 8 | 9 | export const adaptMcpRequestHandler = async < 10 | T extends any, 11 | R extends Error | any 12 | >( 13 | controller: Controller<T, R> 14 | ): Promise<MCPRequestHandler> => { 15 | return async (request: MCPRequest): Promise<MCPResponse> => { 16 | const { params } = request; 17 | const body = params?.arguments as T; 18 | const response = await controller.handle({ 19 | body, 20 | }); 21 | 22 | const isError = response.statusCode < 200 || response.statusCode >= 300; 23 | 24 | return { 25 | tools: [], 26 | isError, 27 | content: [ 28 | { 29 | type: "text", 30 | text: isError 31 | ? JSON.stringify(serializeError(response.body)) 32 | : response.body?.toString(), 33 | }, 34 | ], 35 | }; 36 | }; 37 | }; 38 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/list-project-files/list-project-files-controller.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { badRequest, ok, serverError } from "../../helpers/index.js"; 2 | import { 3 | Controller, 4 | ListProjectFilesRequest, 5 | ListProjectFilesResponse, 6 | ListProjectFilesUseCase, 7 | Request, 8 | Response, 9 | Validator, 10 | } from "./protocols.js"; 11 | 12 | export class ListProjectFilesController 13 | implements Controller<ListProjectFilesRequest, ListProjectFilesResponse> 14 | { 15 | constructor( 16 | private readonly listProjectFilesUseCase: ListProjectFilesUseCase, 17 | private readonly validator: Validator 18 | ) {} 19 | 20 | async handle( 21 | request: Request<ListProjectFilesRequest> 22 | ): Promise<Response<ListProjectFilesResponse>> { 23 | try { 24 | const validationError = this.validator.validate(request.body); 25 | if (validationError) { 26 | return badRequest(validationError); 27 | } 28 | 29 | const { projectName } = request.body!; 30 | 31 | const files = await this.listProjectFilesUseCase.listProjectFiles({ 32 | projectName, 33 | }); 34 | 35 | return ok(files); 36 | } catch (error) { 37 | return serverError(error as Error); 38 | } 39 | } 40 | } 41 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/write/write-controller.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { badRequest, ok, serverError } from "../../helpers/index.js"; 2 | import { 3 | Controller, 4 | Request, 5 | Response, 6 | Validator, 7 | WriteFileUseCase, 8 | WriteRequest, 9 | WriteResponse, 10 | } from "./protocols.js"; 11 | 12 | export class WriteController 13 | implements Controller<WriteRequest, WriteResponse> 14 | { 15 | constructor( 16 | private readonly writeFileUseCase: WriteFileUseCase, 17 | private readonly validator: Validator 18 | ) {} 19 | 20 | async handle( 21 | request: Request<WriteRequest> 22 | ): Promise<Response<WriteResponse>> { 23 | try { 24 | const validationError = this.validator.validate(request.body); 25 | if (validationError) { 26 | return badRequest(validationError); 27 | } 28 | 29 | const { projectName, fileName, content } = request.body!; 30 | 31 | await this.writeFileUseCase.writeFile({ 32 | projectName, 33 | fileName, 34 | content, 35 | }); 36 | 37 | return ok( 38 | `File ${fileName} written successfully to project ${projectName}` 39 | ); 40 | } catch (error) { 41 | return serverError(error as Error); 42 | } 43 | } 44 | } 45 | ``` -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- ```markdown 1 | # Description 2 | 3 | Please include a summary of the changes and which issue is fixed. Include relevant motivation and context. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] Documentation update 15 | 16 | ## Checklist 17 | 18 | - [ ] My code follows the style guidelines of this project 19 | - [ ] I have performed a self-review of my code 20 | - [ ] I have added tests that prove my fix is effective or that my feature works 21 | - [ ] New and existing unit tests pass locally with my changes 22 | - [ ] I have updated the documentation accordingly 23 | - [ ] My changes maintain project isolation and security 24 | - [ ] I have tested my changes with the MCP server running 25 | - [ ] I have verified the memory bank file operations still work correctly 26 | 27 | ## Additional Notes 28 | 29 | Add any other context about the PR here. 30 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "enhancement" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Memory Bank Impact** 19 | How would this feature affect: 20 | 21 | - Project isolation/security 22 | - Memory bank file structure 23 | - MCP tool interactions 24 | - User workflow 25 | - Configuration requirements 26 | 27 | **Additional context** 28 | Add any other context or screenshots about the feature request here. 29 | 30 | **Example Usage** 31 | If applicable, provide an example of how you envision using this feature: 32 | 33 | ```typescript 34 | // Example code or configuration 35 | ``` 36 | 37 | or 38 | 39 | ```json 40 | // Example MCP tool usage or configuration 41 | { 42 | "tool_name": "new_feature", 43 | "arguments": { 44 | "param1": "value1" 45 | } 46 | } 47 | ``` 48 | ``` -------------------------------------------------------------------------------- /src/main/protocols/mcp/adapters/mcp-router-adapter.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | Request as MCPRequest, 3 | ServerResult as MCPResponse, 4 | Tool, 5 | } from "@modelcontextprotocol/sdk/types.js"; 6 | 7 | export type MCPRequestHandler = (request: MCPRequest) => Promise<MCPResponse>; 8 | 9 | export type MCPRoute = { 10 | schema: Tool; 11 | handler: Promise<MCPRequestHandler>; 12 | }; 13 | 14 | export class McpRouterAdapter { 15 | private tools: Map<string, MCPRoute> = new Map(); 16 | 17 | public getToolHandler(name: string): MCPRoute["handler"] | undefined { 18 | return this.tools.get(name)?.handler; 19 | } 20 | 21 | private mapTools(callback: (name: string) => any) { 22 | return Array.from(this.tools.keys()).map(callback); 23 | } 24 | 25 | public getToolsSchemas() { 26 | return Array.from(this.tools.keys()).map( 27 | (name) => this.tools.get(name)?.schema 28 | ); 29 | } 30 | 31 | public getToolCapabilities() { 32 | return Array.from(this.tools.keys()).reduce((acc, name: string) => { 33 | acc[name] = this.tools.get(name)?.schema!; 34 | return acc; 35 | }, {} as Record<string, Tool>); 36 | } 37 | 38 | public setTool({ schema, handler }: MCPRoute): McpRouterAdapter { 39 | this.tools.set(schema.name, { schema, handler }); 40 | return this; 41 | } 42 | } 43 | ``` -------------------------------------------------------------------------------- /src/presentation/controllers/update/update-controller.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { badRequest, notFound, ok, serverError } from "../../helpers/index.js"; 2 | import { 3 | Controller, 4 | Request, 5 | RequestValidator, 6 | Response, 7 | UpdateFileUseCase, 8 | UpdateRequest, 9 | UpdateResponse, 10 | } from "./protocols.js"; 11 | 12 | export class UpdateController 13 | implements Controller<UpdateRequest, UpdateResponse> 14 | { 15 | constructor( 16 | private readonly updateFileUseCase: UpdateFileUseCase, 17 | private readonly validator: RequestValidator 18 | ) {} 19 | 20 | async handle( 21 | request: Request<UpdateRequest> 22 | ): Promise<Response<UpdateResponse>> { 23 | try { 24 | const validationError = this.validator.validate(request.body); 25 | if (validationError) { 26 | return badRequest(validationError); 27 | } 28 | 29 | const { projectName, fileName, content } = request.body!; 30 | 31 | const result = await this.updateFileUseCase.updateFile({ 32 | projectName, 33 | fileName, 34 | content, 35 | }); 36 | 37 | if (result === null) { 38 | return notFound(fileName); 39 | } 40 | 41 | return ok( 42 | `File ${fileName} updated successfully in project ${projectName}` 43 | ); 44 | } catch (error) { 45 | return serverError(error as Error); 46 | } 47 | } 48 | } 49 | ``` -------------------------------------------------------------------------------- /tests/data/usecases/list-projects/list-projects.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { beforeEach, describe, expect, test, vi } from "vitest"; 2 | import { ProjectRepository } from "../../../../src/data/protocols/project-repository.js"; 3 | import { ListProjects } from "../../../../src/data/usecases/list-projects/list-projects.js"; 4 | import { MockProjectRepository } from "../../mocks/index.js"; 5 | 6 | describe("ListProjects UseCase", () => { 7 | let sut: ListProjects; 8 | let projectRepositoryStub: ProjectRepository; 9 | 10 | beforeEach(() => { 11 | projectRepositoryStub = new MockProjectRepository(); 12 | sut = new ListProjects(projectRepositoryStub); 13 | }); 14 | 15 | test("should call ProjectRepository.listProjects()", async () => { 16 | const listProjectsSpy = vi.spyOn(projectRepositoryStub, "listProjects"); 17 | 18 | await sut.listProjects(); 19 | 20 | expect(listProjectsSpy).toHaveBeenCalledTimes(1); 21 | }); 22 | 23 | test("should return a list of projects on success", async () => { 24 | const projects = await sut.listProjects(); 25 | 26 | expect(projects).toEqual(["project-1", "project-2"]); 27 | }); 28 | 29 | test("should propagate errors if repository throws", async () => { 30 | const error = new Error("Repository error"); 31 | vi.spyOn(projectRepositoryStub, "listProjects").mockRejectedValueOnce( 32 | error 33 | ); 34 | 35 | await expect(sut.listProjects()).rejects.toThrow(error); 36 | }); 37 | }); 38 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@allpepper/memory-bank-mcp", 3 | "version": "0.2.1", 4 | "description": "MCP server for remote management of project memory banks", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/alioshr/memory-bank-mcp.git" 8 | }, 9 | "keywords": [ 10 | "mcp", 11 | "memory-bank", 12 | "project-management", 13 | "documentation", 14 | "cline" 15 | ], 16 | "bugs": { 17 | "url": "https://github.com/alioshr/memory-bank-mcp/issues" 18 | }, 19 | "homepage": "https://github.com/alioshr/memory-bank-mcp#readme", 20 | "main": "dist/main/index.js", 21 | "files": [ 22 | "dist" 23 | ], 24 | "author": "Aliosh Pimenta (alioshr)", 25 | "license": "MIT", 26 | "type": "module", 27 | "bin": { 28 | "mcp-server-memory-bank": "dist/main/index.js" 29 | }, 30 | "scripts": { 31 | "build": "tsc && shx chmod +x dist/**/*.js", 32 | "prepare": "npm run build", 33 | "dev": "ts-node src/main/index.ts", 34 | "test": "vitest run", 35 | "test:watch": "vitest", 36 | "test:ui": "vitest --ui", 37 | "test:coverage": "vitest run --coverage" 38 | }, 39 | "dependencies": { 40 | "@modelcontextprotocol/sdk": "^1.5.0", 41 | "fs-extra": "^11.2.0" 42 | }, 43 | "devDependencies": { 44 | "@types/fs-extra": "^11.0.4", 45 | "@types/node": "^20.11.19", 46 | "@vitest/coverage-istanbul": "^3.0.8", 47 | "@vitest/coverage-v8": "^3.1.1", 48 | "@vitest/ui": "^3.0.8", 49 | "shx": "^0.4.0", 50 | "ts-node": "^10.9.2", 51 | "typescript": "^5.8.2", 52 | "vitest": "^3.0.8" 53 | } 54 | } 55 | ``` -------------------------------------------------------------------------------- /tests/presentation/helpers/http-helpers.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { 3 | NotFoundError, 4 | UnexpectedError, 5 | } from "../../../src/presentation/errors/index.js"; 6 | import { 7 | badRequest, 8 | notFound, 9 | ok, 10 | serverError, 11 | } from "../../../src/presentation/helpers/index.js"; 12 | 13 | describe("HTTP Helpers", () => { 14 | describe("badRequest", () => { 15 | it("should return 400 status code and the error", () => { 16 | const error = new Error("any_error"); 17 | const response = badRequest(error); 18 | expect(response).toEqual({ 19 | statusCode: 400, 20 | body: error, 21 | }); 22 | }); 23 | }); 24 | 25 | describe("notFound", () => { 26 | it("should return 404 status code and the error", () => { 27 | const response = notFound("any_error"); 28 | expect(response).toEqual({ 29 | statusCode: 404, 30 | body: new NotFoundError("any_error"), 31 | }); 32 | }); 33 | }); 34 | 35 | describe("serverError", () => { 36 | it("should return 500 status code and wrap the error in UnexpectedError", () => { 37 | const error = new Error("any_error"); 38 | const response = serverError(error); 39 | expect(response).toEqual({ 40 | statusCode: 500, 41 | body: new UnexpectedError(error), 42 | }); 43 | }); 44 | }); 45 | 46 | describe("ok", () => { 47 | it("should return 200 status code and the data", () => { 48 | const data = { name: "any_name" }; 49 | const response = ok(data); 50 | expect(response).toEqual({ 51 | statusCode: 200, 52 | body: data, 53 | }); 54 | }); 55 | }); 56 | }); 57 | ``` -------------------------------------------------------------------------------- /tests/data/mocks/mock-file-repository.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { FileRepository } from "../../../src/data/protocols/file-repository.js"; 2 | 3 | export class MockFileRepository implements FileRepository { 4 | private projectFiles: Record<string, Record<string, string>> = { 5 | "project-1": { 6 | "file1.md": "Content of file1.md", 7 | "file2.md": "Content of file2.md", 8 | }, 9 | "project-2": { 10 | "fileA.md": "Content of fileA.md", 11 | "fileB.md": "Content of fileB.md", 12 | }, 13 | }; 14 | 15 | async listFiles(projectName: string): Promise<string[]> { 16 | return Object.keys(this.projectFiles[projectName] || {}); 17 | } 18 | 19 | async loadFile( 20 | projectName: string, 21 | fileName: string 22 | ): Promise<string | null> { 23 | if ( 24 | this.projectFiles[projectName] && 25 | this.projectFiles[projectName][fileName] 26 | ) { 27 | return this.projectFiles[projectName][fileName]; 28 | } 29 | return null; 30 | } 31 | 32 | async writeFile( 33 | projectName: string, 34 | fileName: string, 35 | content: string 36 | ): Promise<string | null> { 37 | if (!this.projectFiles[projectName]) { 38 | this.projectFiles[projectName] = {}; 39 | } 40 | this.projectFiles[projectName][fileName] = content; 41 | return content; 42 | } 43 | 44 | async updateFile( 45 | projectName: string, 46 | fileName: string, 47 | content: string 48 | ): Promise<string | null> { 49 | if ( 50 | this.projectFiles[projectName] && 51 | this.projectFiles[projectName][fileName] 52 | ) { 53 | this.projectFiles[projectName][fileName] = content; 54 | return content; 55 | } 56 | return null; 57 | } 58 | } 59 | ``` -------------------------------------------------------------------------------- /tests/presentation/controllers/list-projects/list-projects-controller.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it, vi } from "vitest"; 2 | import { ListProjectsController } from "../../../../src/presentation/controllers/list-projects/list-projects-controller.js"; 3 | import { UnexpectedError } from "../../../../src/presentation/errors/index.js"; 4 | import { makeListProjectsUseCase } from "../../mocks/index.js"; 5 | 6 | const makeSut = () => { 7 | const listProjectsUseCaseStub = makeListProjectsUseCase(); 8 | const sut = new ListProjectsController(listProjectsUseCaseStub); 9 | return { 10 | sut, 11 | listProjectsUseCaseStub, 12 | }; 13 | }; 14 | 15 | describe("ListProjectsController", () => { 16 | it("should call ListProjectsUseCase", async () => { 17 | const { sut, listProjectsUseCaseStub } = makeSut(); 18 | const listProjectsSpy = vi.spyOn(listProjectsUseCaseStub, "listProjects"); 19 | await sut.handle(); 20 | expect(listProjectsSpy).toHaveBeenCalled(); 21 | }); 22 | 23 | it("should return 500 if ListProjectsUseCase throws", async () => { 24 | const { sut, listProjectsUseCaseStub } = makeSut(); 25 | vi.spyOn(listProjectsUseCaseStub, "listProjects").mockRejectedValueOnce( 26 | new Error("any_error") 27 | ); 28 | const response = await sut.handle(); 29 | expect(response).toEqual({ 30 | statusCode: 500, 31 | body: new UnexpectedError(new Error("any_error")), 32 | }); 33 | }); 34 | 35 | it("should return 200 with projects on success", async () => { 36 | const { sut } = makeSut(); 37 | const response = await sut.handle(); 38 | expect(response).toEqual({ 39 | statusCode: 200, 40 | body: ["project1", "project2"], 41 | }); 42 | }); 43 | }); 44 | ``` -------------------------------------------------------------------------------- /src/main/protocols/mcp/adapters/mcp-server-adapter.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { 4 | CallToolRequestSchema, 5 | ErrorCode, 6 | ListToolsRequestSchema, 7 | McpError, 8 | ServerResult as MCPResponse, 9 | } from "@modelcontextprotocol/sdk/types.js"; 10 | import { McpRouterAdapter } from "./mcp-router-adapter.js"; 11 | 12 | export class McpServerAdapter { 13 | private server: Server | null = null; 14 | 15 | constructor(private readonly mcpRouter: McpRouterAdapter) {} 16 | 17 | public register({ name, version }: { name: string; version: string }) { 18 | this.server = new Server( 19 | { 20 | name, 21 | version, 22 | }, 23 | { 24 | capabilities: { 25 | tools: this.mcpRouter.getToolCapabilities(), 26 | }, 27 | } 28 | ); 29 | 30 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 31 | tools: this.mcpRouter.getToolsSchemas(), 32 | })); 33 | 34 | this.server.setRequestHandler( 35 | CallToolRequestSchema, 36 | async (request): Promise<MCPResponse> => { 37 | const { name } = request.params; 38 | const handler = await this.mcpRouter.getToolHandler(name); 39 | if (!handler) { 40 | throw new McpError( 41 | ErrorCode.MethodNotFound, 42 | `Tool ${name} not found` 43 | ); 44 | } 45 | return await handler(request); 46 | } 47 | ); 48 | } 49 | 50 | async start(): Promise<void> { 51 | if (!this.server) { 52 | throw new Error("Server not initialized"); 53 | } 54 | 55 | const transport = new StdioServerTransport(); 56 | try { 57 | await this.server.connect(transport); 58 | console.log("Memory Bank MCP server running on stdio"); 59 | } catch (error) { 60 | console.error(error); 61 | } 62 | } 63 | } 64 | ``` -------------------------------------------------------------------------------- /src/infra/filesystem/repositories/fs-project-repository.ts: -------------------------------------------------------------------------------- ```typescript 1 | import fs from "fs-extra"; 2 | import path from "path"; 3 | import { ProjectRepository } from "../../../data/protocols/project-repository.js"; 4 | import { Project } from "../../../domain/entities/index.js"; 5 | 6 | /** 7 | * Filesystem implementation of the ProjectRepository protocol 8 | */ 9 | export class FsProjectRepository implements ProjectRepository { 10 | /** 11 | * Creates a new FsProjectRepository 12 | * @param rootDir The root directory where all projects are stored 13 | */ 14 | constructor(private readonly rootDir: string) {} 15 | 16 | /** 17 | * Builds a path to a project directory 18 | * @param projectName The name of the project 19 | * @returns The full path to the project directory 20 | * @private 21 | */ 22 | private buildProjectPath(projectName: string): string { 23 | return path.join(this.rootDir, projectName); 24 | } 25 | 26 | /** 27 | * Lists all available projects 28 | * @returns An array of Project objects 29 | */ 30 | async listProjects(): Promise<Project[]> { 31 | const entries = await fs.readdir(this.rootDir, { withFileTypes: true }); 32 | const projects: Project[] = entries 33 | .filter((entry) => entry.isDirectory()) 34 | .map((entry) => entry.name); 35 | 36 | return projects; 37 | } 38 | 39 | /** 40 | * Checks if a project exists 41 | * @param name The name of the project 42 | * @returns True if the project exists, false otherwise 43 | */ 44 | async projectExists(name: string): Promise<boolean> { 45 | const projectPath = this.buildProjectPath(name); 46 | // If path doesn't exist, fs.stat will throw an error which will propagate 47 | const stat = await fs.stat(projectPath); 48 | return stat.isDirectory(); 49 | } 50 | 51 | /** 52 | * Ensures a project directory exists, creating it if necessary 53 | * @param name The name of the project 54 | */ 55 | async ensureProject(name: string): Promise<void> { 56 | const projectPath = this.buildProjectPath(name); 57 | await fs.ensureDir(projectPath); 58 | } 59 | } 60 | ``` -------------------------------------------------------------------------------- /tests/validators/validator-composite.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { beforeEach, describe, expect, it } from "vitest"; 2 | import { Validator } from "../../src/presentation/protocols/validator.js"; 3 | import { ValidatorComposite } from "../../src/validators/validator-composite.js"; 4 | 5 | interface TestInput { 6 | field: string; 7 | } 8 | 9 | class ValidatorStub implements Validator { 10 | error: Error | null = null; 11 | callCount = 0; 12 | input: any = null; 13 | 14 | validate(input?: any): Error | null { 15 | this.callCount++; 16 | this.input = input; 17 | return this.error; 18 | } 19 | } 20 | 21 | describe("ValidatorComposite", () => { 22 | let validator1: ValidatorStub; 23 | let validator2: ValidatorStub; 24 | let sut: ValidatorComposite; 25 | 26 | beforeEach(() => { 27 | validator1 = new ValidatorStub(); 28 | validator2 = new ValidatorStub(); 29 | sut = new ValidatorComposite([validator1, validator2]); 30 | }); 31 | 32 | it("should call validate with correct input in all validators", () => { 33 | const input = { field: "any_value" }; 34 | sut.validate(input); 35 | expect(validator1.input).toBe(input); 36 | expect(validator2.input).toBe(input); 37 | }); 38 | 39 | it("should return the first error if any validator fails", () => { 40 | const error = new Error("validator_error"); 41 | validator1.error = error; 42 | 43 | const result = sut.validate({ field: "any_value" }); 44 | 45 | expect(result).toBe(error); 46 | }); 47 | 48 | it("should return the second validator error if the first validator passes", () => { 49 | const error = new Error("validator_error"); 50 | validator2.error = error; 51 | 52 | const result = sut.validate({ field: "any_value" }); 53 | 54 | expect(result).toBe(error); 55 | }); 56 | 57 | it("should return null if all validators pass", () => { 58 | const result = sut.validate({ field: "any_value" }); 59 | 60 | expect(result).toBeNull(); 61 | }); 62 | 63 | it("should call validators in the order they were passed to the constructor", () => { 64 | sut.validate({ field: "any_value" }); 65 | 66 | expect(validator1.callCount).toBe(1); 67 | expect(validator2.callCount).toBe(1); 68 | }); 69 | 70 | it("should stop validating after first error is found", () => { 71 | validator1.error = new Error("validator_error"); 72 | 73 | sut.validate({ field: "any_value" }); 74 | 75 | expect(validator1.callCount).toBe(1); 76 | expect(validator2.callCount).toBe(0); 77 | }); 78 | }); 79 | ``` -------------------------------------------------------------------------------- /tests/infra/filesystem/repositories/fs-project-repository.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import fs from "fs-extra"; 2 | import os from "os"; 3 | import path from "path"; 4 | import { afterEach, beforeEach, describe, expect, it } from "vitest"; 5 | import { FsProjectRepository } from "../../../../src/infra/filesystem/repositories/fs-project-repository.js"; 6 | 7 | describe("FsProjectRepository", () => { 8 | let tempDir: string; 9 | let repository: FsProjectRepository; 10 | 11 | beforeEach(() => { 12 | // Create a temporary directory for tests 13 | tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "memory-bank-test-")); 14 | repository = new FsProjectRepository(tempDir); 15 | }); 16 | 17 | afterEach(() => { 18 | // Clean up after tests 19 | fs.removeSync(tempDir); 20 | }); 21 | 22 | describe("listProjects", () => { 23 | it("should return an empty array when no projects exist", async () => { 24 | const result = await repository.listProjects(); 25 | expect(result).toEqual([]); 26 | }); 27 | 28 | it("should return project directories as Project objects", async () => { 29 | // Create test directories 30 | await fs.mkdir(path.join(tempDir, "project1")); 31 | await fs.mkdir(path.join(tempDir, "project2")); 32 | // Create a file to ensure it's not returned 33 | await fs.writeFile(path.join(tempDir, "not-a-project.txt"), "test"); 34 | 35 | const result = await repository.listProjects(); 36 | 37 | expect(result).toHaveLength(2); 38 | expect(result).toEqual(expect.arrayContaining(["project1", "project2"])); 39 | }); 40 | }); 41 | 42 | describe("projectExists", () => { 43 | it("should throw an error when project path cannot be accessed", async () => { 44 | // We now let errors propagate, so stat errors will throw 45 | const nonExistentProject = "non-existent-project"; 46 | 47 | await expect( 48 | repository.projectExists(nonExistentProject) 49 | ).rejects.toThrow(); 50 | }); 51 | 52 | it("should return true when project exists", async () => { 53 | await fs.mkdir(path.join(tempDir, "existing-project")); 54 | 55 | const result = await repository.projectExists("existing-project"); 56 | 57 | expect(result).toBe(true); 58 | }); 59 | }); 60 | 61 | describe("ensureProject", () => { 62 | it("should create project directory if it does not exist", async () => { 63 | await repository.ensureProject("new-project"); 64 | 65 | const projectPath = path.join(tempDir, "new-project"); 66 | const exists = await fs.pathExists(projectPath); 67 | 68 | expect(exists).toBe(true); 69 | }); 70 | 71 | it("should not throw if project directory already exists", async () => { 72 | await fs.mkdir(path.join(tempDir, "existing-project")); 73 | 74 | await expect( 75 | repository.ensureProject("existing-project") 76 | ).resolves.not.toThrow(); 77 | }); 78 | }); 79 | }); 80 | ``` -------------------------------------------------------------------------------- /tests/data/usecases/list-project-files/list-project-files.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { beforeEach, describe, expect, test, vi } from "vitest"; 2 | import { FileRepository } from "../../../../src/data/protocols/file-repository.js"; 3 | import { ProjectRepository } from "../../../../src/data/protocols/project-repository.js"; 4 | import { ListProjectFiles } from "../../../../src/data/usecases/list-project-files/list-project-files.js"; 5 | import { ListProjectFilesParams } from "../../../../src/domain/usecases/list-project-files.js"; 6 | import { 7 | MockFileRepository, 8 | MockProjectRepository, 9 | } from "../../mocks/index.js"; 10 | 11 | describe("ListProjectFiles UseCase", () => { 12 | let sut: ListProjectFiles; 13 | let fileRepositoryStub: FileRepository; 14 | let projectRepositoryStub: ProjectRepository; 15 | 16 | beforeEach(() => { 17 | fileRepositoryStub = new MockFileRepository(); 18 | projectRepositoryStub = new MockProjectRepository(); 19 | sut = new ListProjectFiles(fileRepositoryStub, projectRepositoryStub); 20 | }); 21 | 22 | test("should call ProjectRepository.projectExists with correct projectName", async () => { 23 | const projectExistsSpy = vi.spyOn(projectRepositoryStub, "projectExists"); 24 | const params: ListProjectFilesParams = { projectName: "project-1" }; 25 | 26 | await sut.listProjectFiles(params); 27 | 28 | expect(projectExistsSpy).toHaveBeenCalledWith("project-1"); 29 | }); 30 | 31 | test("should return empty array if project does not exist", async () => { 32 | vi.spyOn(projectRepositoryStub, "projectExists").mockResolvedValueOnce( 33 | false 34 | ); 35 | const params: ListProjectFilesParams = { 36 | projectName: "non-existent-project", 37 | }; 38 | 39 | const result = await sut.listProjectFiles(params); 40 | 41 | expect(result).toEqual([]); 42 | }); 43 | 44 | test("should call FileRepository.listFiles with correct projectName if project exists", async () => { 45 | vi.spyOn(projectRepositoryStub, "projectExists").mockResolvedValueOnce( 46 | true 47 | ); 48 | const listFilesSpy = vi.spyOn(fileRepositoryStub, "listFiles"); 49 | const params: ListProjectFilesParams = { projectName: "project-1" }; 50 | 51 | await sut.listProjectFiles(params); 52 | 53 | expect(listFilesSpy).toHaveBeenCalledWith("project-1"); 54 | }); 55 | 56 | test("should return files list on success", async () => { 57 | const params: ListProjectFilesParams = { projectName: "project-1" }; 58 | 59 | const files = await sut.listProjectFiles(params); 60 | 61 | expect(files).toEqual(["file1.md", "file2.md"]); 62 | }); 63 | 64 | test("should propagate errors if repository throws", async () => { 65 | const error = new Error("Repository error"); 66 | vi.spyOn(projectRepositoryStub, "projectExists").mockRejectedValueOnce( 67 | error 68 | ); 69 | const params: ListProjectFilesParams = { projectName: "project-1" }; 70 | 71 | await expect(sut.listProjectFiles(params)).rejects.toThrow(error); 72 | }); 73 | }); 74 | ``` -------------------------------------------------------------------------------- /tests/validators/path-security-validator.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { InvalidParamError } from "../../src/presentation/errors/index.js"; 3 | import { PathSecurityValidator } from "../../src/validators/path-security-validator.js"; 4 | 5 | describe("PathSecurityValidator", () => { 6 | it("should return null if field is not provided", () => { 7 | const sut = new PathSecurityValidator("field"); 8 | const input = {}; 9 | const error = sut.validate(input); 10 | 11 | expect(error).toBeNull(); 12 | }); 13 | 14 | it("should return null if input is null", () => { 15 | const sut = new PathSecurityValidator("field"); 16 | const error = sut.validate(null); 17 | 18 | expect(error).toBeNull(); 19 | }); 20 | 21 | it("should return null if input is undefined", () => { 22 | const sut = new PathSecurityValidator("field"); 23 | const error = sut.validate(undefined); 24 | 25 | expect(error).toBeNull(); 26 | }); 27 | 28 | it("should return InvalidParamError if field contains directory traversal (..)", () => { 29 | const sut = new PathSecurityValidator("field"); 30 | const input = { field: "something/../etc/passwd" }; 31 | const error = sut.validate(input); 32 | 33 | expect(error).toBeInstanceOf(InvalidParamError); 34 | expect(error?.message).toBe( 35 | "Invalid parameter: field contains invalid path segments" 36 | ); 37 | }); 38 | 39 | it("should return InvalidParamError if field contains directory traversal (..) even without slashes", () => { 40 | const sut = new PathSecurityValidator("field"); 41 | const input = { field: "something..etc" }; 42 | const error = sut.validate(input); 43 | 44 | expect(error).toBeInstanceOf(InvalidParamError); 45 | expect(error?.message).toBe( 46 | "Invalid parameter: field contains invalid path segments" 47 | ); 48 | }); 49 | 50 | it("should return InvalidParamError if field contains forward slashes", () => { 51 | const sut = new PathSecurityValidator("field"); 52 | const input = { field: "path/to/file" }; 53 | const error = sut.validate(input); 54 | 55 | expect(error).toBeInstanceOf(InvalidParamError); 56 | expect(error?.message).toBe( 57 | "Invalid parameter: field contains invalid path segments" 58 | ); 59 | }); 60 | 61 | it("should return null if field is a valid string without path segments", () => { 62 | const sut = new PathSecurityValidator("field"); 63 | const input = { field: "validname123" }; 64 | const error = sut.validate(input); 65 | 66 | expect(error).toBeNull(); 67 | }); 68 | 69 | it("should return null if field contains periods but not double periods", () => { 70 | const sut = new PathSecurityValidator("field"); 71 | const input = { field: "filename.txt" }; 72 | const error = sut.validate(input); 73 | 74 | expect(error).toBeNull(); 75 | }); 76 | 77 | it("should ignore non-string fields", () => { 78 | const sut = new PathSecurityValidator("field"); 79 | const input = { field: 123 as any }; 80 | const error = sut.validate(input); 81 | 82 | expect(error).toBeNull(); 83 | }); 84 | }); 85 | ``` -------------------------------------------------------------------------------- /tests/validators/required-field-validator.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { MissingParamError } from "../../src/presentation/errors/index.js"; 3 | import { RequiredFieldValidator } from "../../src/validators/required-field-validator.js"; 4 | 5 | describe("RequiredFieldValidator", () => { 6 | it("should return MissingParamError if field is not provided", () => { 7 | const sut = new RequiredFieldValidator("field"); 8 | const input = {}; 9 | const error = sut.validate(input); 10 | 11 | expect(error).toBeInstanceOf(MissingParamError); 12 | expect(error?.message).toBe("Missing parameter: field"); 13 | }); 14 | 15 | it("should return MissingParamError if field is undefined", () => { 16 | const sut = new RequiredFieldValidator("field"); 17 | const input = { field: undefined }; 18 | const error = sut.validate(input); 19 | 20 | expect(error).toBeInstanceOf(MissingParamError); 21 | expect(error?.message).toBe("Missing parameter: field"); 22 | }); 23 | 24 | it("should return MissingParamError if field is null", () => { 25 | const sut = new RequiredFieldValidator("field"); 26 | const input = { field: null }; 27 | const error = sut.validate(input); 28 | 29 | expect(error).toBeInstanceOf(MissingParamError); 30 | expect(error?.message).toBe("Missing parameter: field"); 31 | }); 32 | 33 | it("should return MissingParamError if field is empty string", () => { 34 | const sut = new RequiredFieldValidator("fieldEmpty"); 35 | const input = { fieldEmpty: "" }; 36 | const error = sut.validate(input); 37 | 38 | expect(error).toBeInstanceOf(MissingParamError); 39 | expect(error?.message).toBe("Missing parameter: fieldEmpty"); 40 | }); 41 | 42 | it("should return MissingParamError if input is null", () => { 43 | const sut = new RequiredFieldValidator("field"); 44 | const error = sut.validate(null); 45 | 46 | expect(error).toBeInstanceOf(MissingParamError); 47 | expect(error?.message).toBe("Missing parameter: field"); 48 | }); 49 | 50 | it("should return MissingParamError if input is undefined", () => { 51 | const sut = new RequiredFieldValidator("field"); 52 | const error = sut.validate(undefined); 53 | 54 | expect(error).toBeInstanceOf(MissingParamError); 55 | expect(error?.message).toBe("Missing parameter: field"); 56 | }); 57 | 58 | it("should not return error if field is zero", () => { 59 | const sut = new RequiredFieldValidator("fieldZero"); 60 | const input = { fieldZero: 0 }; 61 | const error = sut.validate(input); 62 | 63 | expect(error).toBeNull(); 64 | }); 65 | 66 | it("should not return error if field is false", () => { 67 | const sut = new RequiredFieldValidator("fieldFalse"); 68 | const input = { fieldFalse: false }; 69 | const error = sut.validate(input); 70 | 71 | expect(error).toBeNull(); 72 | }); 73 | 74 | it("should not return error if field is provided", () => { 75 | const sut = new RequiredFieldValidator("field"); 76 | const input = { field: "any_value" }; 77 | const error = sut.validate(input); 78 | 79 | expect(error).toBeNull(); 80 | }); 81 | }); 82 | ``` -------------------------------------------------------------------------------- /tests/validators/param-name-validator.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { InvalidParamError } from "../../src/presentation/errors/index.js"; 3 | import { ParamNameValidator } from "../../src/validators/param-name-validator.js"; 4 | 5 | describe("ParamNameValidator", () => { 6 | it("should return null if field is not provided", () => { 7 | const sut = new ParamNameValidator("field"); 8 | const input = {}; 9 | const error = sut.validate(input); 10 | 11 | expect(error).toBeNull(); 12 | }); 13 | 14 | it("should return null if input is null", () => { 15 | const sut = new ParamNameValidator("field"); 16 | const error = sut.validate(null); 17 | 18 | expect(error).toBeNull(); 19 | }); 20 | 21 | it("should return null if input is undefined", () => { 22 | const sut = new ParamNameValidator("field"); 23 | const error = sut.validate(undefined); 24 | 25 | expect(error).toBeNull(); 26 | }); 27 | 28 | it("should return InvalidParamError if field doesn't match regex (contains special characters)", () => { 29 | const sut = new ParamNameValidator("field"); 30 | const input = { field: "invalid/name" }; 31 | const error = sut.validate(input); 32 | 33 | expect(error).toBeInstanceOf(InvalidParamError); 34 | expect(error?.message).toBe("Invalid parameter: invalid/name"); 35 | }); 36 | 37 | it("should return InvalidParamError if field doesn't match regex (contains spaces)", () => { 38 | const sut = new ParamNameValidator("field"); 39 | const input = { field: "invalid name" }; 40 | const error = sut.validate(input); 41 | 42 | expect(error).toBeInstanceOf(InvalidParamError); 43 | expect(error?.message).toBe("Invalid parameter: invalid name"); 44 | }); 45 | 46 | it("should return null if field matches NAME_REGEX (alphanumeric only)", () => { 47 | const sut = new ParamNameValidator("field"); 48 | const input = { field: "validname123" }; 49 | const error = sut.validate(input); 50 | 51 | expect(error).toBeNull(); 52 | }); 53 | 54 | it("should return null if field matches NAME_REGEX (with underscores)", () => { 55 | const sut = new ParamNameValidator("field"); 56 | const input = { field: "valid_name_123" }; 57 | const error = sut.validate(input); 58 | 59 | expect(error).toBeNull(); 60 | }); 61 | 62 | it("should return null if field matches NAME_REGEX (with hyphens)", () => { 63 | const sut = new ParamNameValidator("field"); 64 | const input = { field: "valid-name-123" }; 65 | const error = sut.validate(input); 66 | 67 | expect(error).toBeNull(); 68 | }); 69 | 70 | it("should use provided regex instead of default NAME_REGEX if given", () => { 71 | // Custom regex that only allows lowercase letters 72 | const customRegex = /^[a-z]+$/; 73 | const sut = new ParamNameValidator("field", customRegex); 74 | 75 | const validInput = { field: "validname" }; 76 | const validError = sut.validate(validInput); 77 | expect(validError).toBeNull(); 78 | 79 | const invalidInput = { field: "Invalid123" }; 80 | const invalidError = sut.validate(invalidInput); 81 | expect(invalidError).toBeInstanceOf(InvalidParamError); 82 | }); 83 | }); 84 | ``` -------------------------------------------------------------------------------- /src/infra/filesystem/repositories/fs-file-repository.ts: -------------------------------------------------------------------------------- ```typescript 1 | import fs from "fs-extra"; 2 | import path from "path"; 3 | import { FileRepository } from "../../../data/protocols/file-repository.js"; 4 | import { File } from "../../../domain/entities/index.js"; 5 | /** 6 | * Filesystem implementation of the FileRepository protocol 7 | */ 8 | export class FsFileRepository implements FileRepository { 9 | /** 10 | * Creates a new FsFileRepository 11 | * @param rootDir The root directory where all projects are stored 12 | */ 13 | constructor(private readonly rootDir: string) {} 14 | 15 | /** 16 | * Lists all files in a project 17 | * @param projectName The name of the project 18 | * @returns An array of file names 19 | */ 20 | async listFiles(projectName: string): Promise<File[]> { 21 | const projectPath = path.join(this.rootDir, projectName); 22 | 23 | const projectExists = await fs.pathExists(projectPath); 24 | if (!projectExists) { 25 | return []; 26 | } 27 | 28 | const entries = await fs.readdir(projectPath, { withFileTypes: true }); 29 | return entries.filter((entry) => entry.isFile()).map((entry) => entry.name); 30 | } 31 | 32 | /** 33 | * Loads the content of a file 34 | * @param projectName The name of the project 35 | * @param fileName The name of the file 36 | * @returns The content of the file or null if the file doesn't exist 37 | */ 38 | async loadFile( 39 | projectName: string, 40 | fileName: string 41 | ): Promise<string | null> { 42 | const filePath = path.join(this.rootDir, projectName, fileName); 43 | 44 | const fileExists = await fs.pathExists(filePath); 45 | if (!fileExists) { 46 | return null; 47 | } 48 | 49 | const content = await fs.readFile(filePath, "utf-8"); 50 | return content; 51 | } 52 | 53 | /** 54 | * Writes a new file 55 | * @param projectName The name of the project 56 | * @param fileName The name of the file 57 | * @param content The content to write 58 | * @returns The content of the file after writing, or null if the file already exists 59 | */ 60 | async writeFile( 61 | projectName: string, 62 | fileName: string, 63 | content: string 64 | ): Promise<File | null> { 65 | const projectPath = path.join(this.rootDir, projectName); 66 | await fs.ensureDir(projectPath); 67 | 68 | const filePath = path.join(projectPath, fileName); 69 | 70 | const fileExists = await fs.pathExists(filePath); 71 | if (fileExists) { 72 | return null; 73 | } 74 | 75 | await fs.writeFile(filePath, content, "utf-8"); 76 | 77 | return await this.loadFile(projectName, fileName); 78 | } 79 | 80 | /** 81 | * Updates an existing file 82 | * @param projectName The name of the project 83 | * @param fileName The name of the file 84 | * @param content The new content 85 | * @returns The content of the file after updating, or null if the file doesn't exist 86 | */ 87 | async updateFile( 88 | projectName: string, 89 | fileName: string, 90 | content: string 91 | ): Promise<File | null> { 92 | const filePath = path.join(this.rootDir, projectName, fileName); 93 | 94 | const fileExists = await fs.pathExists(filePath); 95 | if (!fileExists) { 96 | return null; 97 | } 98 | 99 | await fs.writeFile(filePath, content, "utf-8"); 100 | 101 | return await this.loadFile(projectName, fileName); 102 | } 103 | } 104 | ``` -------------------------------------------------------------------------------- /tests/data/usecases/read-file/read-file.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { beforeEach, describe, expect, test, vi } from "vitest"; 2 | import { FileRepository } from "../../../../src/data/protocols/file-repository.js"; 3 | import { ProjectRepository } from "../../../../src/data/protocols/project-repository.js"; 4 | import { ReadFile } from "../../../../src/data/usecases/read-file/read-file.js"; 5 | import { ReadFileParams } from "../../../../src/domain/usecases/read-file.js"; 6 | import { 7 | MockFileRepository, 8 | MockProjectRepository, 9 | } from "../../mocks/index.js"; 10 | 11 | describe("ReadFile UseCase", () => { 12 | let sut: ReadFile; 13 | let fileRepositoryStub: FileRepository; 14 | let projectRepositoryStub: ProjectRepository; 15 | 16 | beforeEach(() => { 17 | fileRepositoryStub = new MockFileRepository(); 18 | projectRepositoryStub = new MockProjectRepository(); 19 | sut = new ReadFile(fileRepositoryStub, projectRepositoryStub); 20 | }); 21 | 22 | test("should call ProjectRepository.projectExists with correct projectName", async () => { 23 | const projectExistsSpy = vi.spyOn(projectRepositoryStub, "projectExists"); 24 | const params: ReadFileParams = { 25 | projectName: "project-1", 26 | fileName: "file1.md", 27 | }; 28 | 29 | await sut.readFile(params); 30 | 31 | expect(projectExistsSpy).toHaveBeenCalledWith("project-1"); 32 | }); 33 | 34 | test("should return null if project does not exist", async () => { 35 | vi.spyOn(projectRepositoryStub, "projectExists").mockResolvedValueOnce( 36 | false 37 | ); 38 | const params: ReadFileParams = { 39 | projectName: "non-existent-project", 40 | fileName: "file1.md", 41 | }; 42 | 43 | const result = await sut.readFile(params); 44 | 45 | expect(result).toBeNull(); 46 | }); 47 | 48 | test("should call FileRepository.loadFile with correct params if project exists", async () => { 49 | const loadFileSpy = vi.spyOn(fileRepositoryStub, "loadFile"); 50 | const params: ReadFileParams = { 51 | projectName: "project-1", 52 | fileName: "file1.md", 53 | }; 54 | 55 | await sut.readFile(params); 56 | 57 | expect(loadFileSpy).toHaveBeenCalledWith("project-1", "file1.md"); 58 | }); 59 | 60 | test("should return file content on success", async () => { 61 | const params: ReadFileParams = { 62 | projectName: "project-1", 63 | fileName: "file1.md", 64 | }; 65 | 66 | const content = await sut.readFile(params); 67 | 68 | expect(content).toBe("Content of file1.md"); 69 | }); 70 | 71 | test("should return null if file does not exist", async () => { 72 | vi.spyOn(fileRepositoryStub, "loadFile").mockResolvedValueOnce(null); 73 | const params: ReadFileParams = { 74 | projectName: "project-1", 75 | fileName: "non-existent-file.md", 76 | }; 77 | 78 | const content = await sut.readFile(params); 79 | 80 | expect(content).toBeNull(); 81 | }); 82 | 83 | test("should propagate errors if repository throws", async () => { 84 | const error = new Error("Repository error"); 85 | vi.spyOn(projectRepositoryStub, "projectExists").mockRejectedValueOnce( 86 | error 87 | ); 88 | const params: ReadFileParams = { 89 | projectName: "project-1", 90 | fileName: "file1.md", 91 | }; 92 | 93 | await expect(sut.readFile(params)).rejects.toThrow(error); 94 | }); 95 | }); 96 | ``` -------------------------------------------------------------------------------- /tests/presentation/controllers/list-project-files/list-project-files-controller.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it, vi } from "vitest"; 2 | import { ListProjectFilesController } from "../../../../src/presentation/controllers/list-project-files/list-project-files-controller.js"; 3 | import { ListProjectFilesRequest } from "../../../../src/presentation/controllers/list-project-files/protocols.js"; 4 | import { UnexpectedError } from "../../../../src/presentation/errors/index.js"; 5 | import { 6 | makeListProjectFilesUseCase, 7 | makeValidator, 8 | } from "../../mocks/index.js"; 9 | 10 | const makeSut = () => { 11 | const validatorStub = makeValidator<ListProjectFilesRequest>(); 12 | const listProjectFilesUseCaseStub = makeListProjectFilesUseCase(); 13 | const sut = new ListProjectFilesController( 14 | listProjectFilesUseCaseStub, 15 | validatorStub 16 | ); 17 | return { 18 | sut, 19 | validatorStub, 20 | listProjectFilesUseCaseStub, 21 | }; 22 | }; 23 | 24 | describe("ListProjectFilesController", () => { 25 | it("should call validator with correct values", async () => { 26 | const { sut, validatorStub } = makeSut(); 27 | const validateSpy = vi.spyOn(validatorStub, "validate"); 28 | const request = { 29 | body: { 30 | projectName: "any_project", 31 | }, 32 | }; 33 | await sut.handle(request); 34 | expect(validateSpy).toHaveBeenCalledWith(request.body); 35 | }); 36 | 37 | it("should return 400 if validator returns an error", async () => { 38 | const { sut, validatorStub } = makeSut(); 39 | vi.spyOn(validatorStub, "validate").mockReturnValueOnce( 40 | new Error("any_error") 41 | ); 42 | const request = { 43 | body: { 44 | projectName: "any_project", 45 | }, 46 | }; 47 | const response = await sut.handle(request); 48 | expect(response).toEqual({ 49 | statusCode: 400, 50 | body: new Error("any_error"), 51 | }); 52 | }); 53 | 54 | it("should call ListProjectFilesUseCase with correct values", async () => { 55 | const { sut, listProjectFilesUseCaseStub } = makeSut(); 56 | const listProjectFilesSpy = vi.spyOn( 57 | listProjectFilesUseCaseStub, 58 | "listProjectFiles" 59 | ); 60 | const request = { 61 | body: { 62 | projectName: "any_project", 63 | }, 64 | }; 65 | await sut.handle(request); 66 | expect(listProjectFilesSpy).toHaveBeenCalledWith({ 67 | projectName: "any_project", 68 | }); 69 | }); 70 | 71 | it("should return 500 if ListProjectFilesUseCase throws", async () => { 72 | const { sut, listProjectFilesUseCaseStub } = makeSut(); 73 | vi.spyOn( 74 | listProjectFilesUseCaseStub, 75 | "listProjectFiles" 76 | ).mockRejectedValueOnce(new Error("any_error")); 77 | const request = { 78 | body: { 79 | projectName: "any_project", 80 | }, 81 | }; 82 | const response = await sut.handle(request); 83 | expect(response).toEqual({ 84 | statusCode: 500, 85 | body: new UnexpectedError(new Error("any_error")), 86 | }); 87 | }); 88 | 89 | it("should return 200 with files on success", async () => { 90 | const { sut } = makeSut(); 91 | const request = { 92 | body: { 93 | projectName: "any_project", 94 | }, 95 | }; 96 | const response = await sut.handle(request); 97 | expect(response).toEqual({ 98 | statusCode: 200, 99 | body: ["file1.txt", "file2.txt"], 100 | }); 101 | }); 102 | }); 103 | ``` -------------------------------------------------------------------------------- /src/main/protocols/mcp/routes.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | makeListProjectFilesController, 3 | makeListProjectsController, 4 | makeReadController, 5 | makeUpdateController, 6 | makeWriteController, 7 | } from "../../factories/controllers/index.js"; 8 | import { adaptMcpRequestHandler } from "./adapters/mcp-request-adapter.js"; 9 | import { McpRouterAdapter } from "./adapters/mcp-router-adapter.js"; 10 | 11 | export default () => { 12 | const router = new McpRouterAdapter(); 13 | 14 | router.setTool({ 15 | schema: { 16 | name: "list_projects", 17 | description: "List all projects in the memory bank", 18 | inputSchema: { 19 | type: "object", 20 | properties: {}, 21 | required: [], 22 | }, 23 | }, 24 | handler: adaptMcpRequestHandler(makeListProjectsController()), 25 | }); 26 | 27 | router.setTool({ 28 | schema: { 29 | name: "list_project_files", 30 | description: "List all files within a specific project", 31 | inputSchema: { 32 | type: "object", 33 | properties: { 34 | projectName: { 35 | type: "string", 36 | description: "The name of the project", 37 | }, 38 | }, 39 | required: ["projectName"], 40 | }, 41 | }, 42 | handler: adaptMcpRequestHandler(makeListProjectFilesController()), 43 | }); 44 | 45 | router.setTool({ 46 | schema: { 47 | name: "memory_bank_read", 48 | description: "Read a memory bank file for a specific project", 49 | inputSchema: { 50 | type: "object", 51 | properties: { 52 | projectName: { 53 | type: "string", 54 | description: "The name of the project", 55 | }, 56 | fileName: { 57 | type: "string", 58 | description: "The name of the file", 59 | }, 60 | }, 61 | required: ["projectName", "fileName"], 62 | }, 63 | }, 64 | handler: adaptMcpRequestHandler(makeReadController()), 65 | }); 66 | 67 | router.setTool({ 68 | schema: { 69 | name: "memory_bank_write", 70 | description: "Create a new memory bank file for a specific project", 71 | inputSchema: { 72 | type: "object", 73 | properties: { 74 | projectName: { 75 | type: "string", 76 | description: "The name of the project", 77 | }, 78 | fileName: { 79 | type: "string", 80 | description: "The name of the file", 81 | }, 82 | content: { 83 | type: "string", 84 | description: "The content of the file", 85 | }, 86 | }, 87 | required: ["projectName", "fileName", "content"], 88 | }, 89 | }, 90 | handler: adaptMcpRequestHandler(makeWriteController()), 91 | }); 92 | 93 | router.setTool({ 94 | schema: { 95 | name: "memory_bank_update", 96 | description: "Update an existing memory bank file for a specific project", 97 | inputSchema: { 98 | type: "object", 99 | properties: { 100 | projectName: { 101 | type: "string", 102 | description: "The name of the project", 103 | }, 104 | fileName: { 105 | type: "string", 106 | description: "The name of the file", 107 | }, 108 | content: { 109 | type: "string", 110 | description: "The content of the file", 111 | }, 112 | }, 113 | required: ["projectName", "fileName", "content"], 114 | }, 115 | }, 116 | handler: adaptMcpRequestHandler(makeUpdateController()), 117 | }); 118 | 119 | return router; 120 | }; 121 | ``` -------------------------------------------------------------------------------- /tests/presentation/controllers/write/write-controller.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it, vi } from "vitest"; 2 | import { WriteRequest } from "../../../../src/presentation/controllers/write/protocols.js"; 3 | import { WriteController } from "../../../../src/presentation/controllers/write/write-controller.js"; 4 | import { UnexpectedError } from "../../../../src/presentation/errors/index.js"; 5 | import { makeValidator, makeWriteFileUseCase } from "../../mocks/index.js"; 6 | 7 | const makeSut = () => { 8 | const validatorStub = makeValidator<WriteRequest>(); 9 | const writeFileUseCaseStub = makeWriteFileUseCase(); 10 | const sut = new WriteController(writeFileUseCaseStub, validatorStub); 11 | return { 12 | sut, 13 | validatorStub, 14 | writeFileUseCaseStub, 15 | }; 16 | }; 17 | 18 | describe("WriteController", () => { 19 | it("should call validator with correct values", async () => { 20 | const { sut, validatorStub } = makeSut(); 21 | const validateSpy = vi.spyOn(validatorStub, "validate"); 22 | const request = { 23 | body: { 24 | projectName: "any_project", 25 | fileName: "any_file", 26 | content: "any_content", 27 | }, 28 | }; 29 | await sut.handle(request); 30 | expect(validateSpy).toHaveBeenCalledWith(request.body); 31 | }); 32 | 33 | it("should return 400 if validator returns an error", async () => { 34 | const { sut, validatorStub } = makeSut(); 35 | vi.spyOn(validatorStub, "validate").mockReturnValueOnce( 36 | new Error("any_error") 37 | ); 38 | const request = { 39 | body: { 40 | projectName: "any_project", 41 | fileName: "any_file", 42 | content: "any_content", 43 | }, 44 | }; 45 | const response = await sut.handle(request); 46 | expect(response).toEqual({ 47 | statusCode: 400, 48 | body: new Error("any_error"), 49 | }); 50 | }); 51 | 52 | it("should call WriteFileUseCase with correct values", async () => { 53 | const { sut, writeFileUseCaseStub } = makeSut(); 54 | const writeFileSpy = vi.spyOn(writeFileUseCaseStub, "writeFile"); 55 | const request = { 56 | body: { 57 | projectName: "any_project", 58 | fileName: "any_file", 59 | content: "any_content", 60 | }, 61 | }; 62 | await sut.handle(request); 63 | expect(writeFileSpy).toHaveBeenCalledWith({ 64 | projectName: "any_project", 65 | fileName: "any_file", 66 | content: "any_content", 67 | }); 68 | }); 69 | 70 | it("should return 500 if WriteFileUseCase throws", async () => { 71 | const { sut, writeFileUseCaseStub } = makeSut(); 72 | vi.spyOn(writeFileUseCaseStub, "writeFile").mockRejectedValueOnce( 73 | new Error("any_error") 74 | ); 75 | const request = { 76 | body: { 77 | projectName: "any_project", 78 | fileName: "any_file", 79 | content: "any_content", 80 | }, 81 | }; 82 | const response = await sut.handle(request); 83 | expect(response).toEqual({ 84 | statusCode: 500, 85 | body: new UnexpectedError(new Error("any_error")), 86 | }); 87 | }); 88 | 89 | it("should return 200 if valid data is provided", async () => { 90 | const { sut } = makeSut(); 91 | const request = { 92 | body: { 93 | projectName: "any_project", 94 | fileName: "any_file", 95 | content: "any_content", 96 | }, 97 | }; 98 | const response = await sut.handle(request); 99 | expect(response).toEqual({ 100 | statusCode: 200, 101 | body: "File any_file written successfully to project any_project", 102 | }); 103 | }); 104 | }); 105 | ``` -------------------------------------------------------------------------------- /tests/presentation/controllers/read/read-controller.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it, vi } from "vitest"; 2 | import { ReadRequest } from "../../../../src/presentation/controllers/read/protocols.js"; 3 | import { ReadController } from "../../../../src/presentation/controllers/read/read-controller.js"; 4 | import { 5 | NotFoundError, 6 | UnexpectedError, 7 | } from "../../../../src/presentation/errors/index.js"; 8 | import { makeReadFileUseCase, makeValidator } from "../../mocks/index.js"; 9 | 10 | const makeSut = () => { 11 | const validatorStub = makeValidator<ReadRequest>(); 12 | const readFileUseCaseStub = makeReadFileUseCase(); 13 | const sut = new ReadController(readFileUseCaseStub, validatorStub); 14 | return { 15 | sut, 16 | validatorStub, 17 | readFileUseCaseStub, 18 | }; 19 | }; 20 | 21 | describe("ReadController", () => { 22 | it("should call validator with correct values", async () => { 23 | const { sut, validatorStub } = makeSut(); 24 | const validateSpy = vi.spyOn(validatorStub, "validate"); 25 | const request = { 26 | body: { 27 | projectName: "any_project", 28 | fileName: "any_file", 29 | }, 30 | }; 31 | await sut.handle(request); 32 | expect(validateSpy).toHaveBeenCalledWith(request.body); 33 | }); 34 | 35 | it("should return 400 if validator returns an error", async () => { 36 | const { sut, validatorStub } = makeSut(); 37 | vi.spyOn(validatorStub, "validate").mockReturnValueOnce( 38 | new Error("any_error") 39 | ); 40 | const request = { 41 | body: { 42 | projectName: "any_project", 43 | fileName: "any_file", 44 | }, 45 | }; 46 | const response = await sut.handle(request); 47 | expect(response).toEqual({ 48 | statusCode: 400, 49 | body: new Error("any_error"), 50 | }); 51 | }); 52 | 53 | it("should call ReadFileUseCase with correct values", async () => { 54 | const { sut, readFileUseCaseStub } = makeSut(); 55 | const readFileSpy = vi.spyOn(readFileUseCaseStub, "readFile"); 56 | const request = { 57 | body: { 58 | projectName: "any_project", 59 | fileName: "any_file", 60 | }, 61 | }; 62 | await sut.handle(request); 63 | expect(readFileSpy).toHaveBeenCalledWith({ 64 | projectName: "any_project", 65 | fileName: "any_file", 66 | }); 67 | }); 68 | 69 | it("should return 404 if ReadFileUseCase returns null", async () => { 70 | const { sut, readFileUseCaseStub } = makeSut(); 71 | vi.spyOn(readFileUseCaseStub, "readFile").mockResolvedValueOnce(null); 72 | const request = { 73 | body: { 74 | projectName: "any_project", 75 | fileName: "any_file", 76 | }, 77 | }; 78 | const response = await sut.handle(request); 79 | expect(response).toEqual({ 80 | statusCode: 404, 81 | body: new NotFoundError("any_file"), 82 | }); 83 | }); 84 | 85 | it("should return 500 if ReadFileUseCase throws", async () => { 86 | const { sut, readFileUseCaseStub } = makeSut(); 87 | vi.spyOn(readFileUseCaseStub, "readFile").mockRejectedValueOnce( 88 | new Error("any_error") 89 | ); 90 | const request = { 91 | body: { 92 | projectName: "any_project", 93 | fileName: "any_file", 94 | }, 95 | }; 96 | const response = await sut.handle(request); 97 | expect(response).toEqual({ 98 | statusCode: 500, 99 | body: new UnexpectedError(new Error("any_error")), 100 | }); 101 | }); 102 | 103 | it("should return 200 if valid data is provided", async () => { 104 | const { sut } = makeSut(); 105 | const request = { 106 | body: { 107 | projectName: "any_project", 108 | fileName: "any_file", 109 | }, 110 | }; 111 | const response = await sut.handle(request); 112 | expect(response).toEqual({ 113 | statusCode: 200, 114 | body: "file content", 115 | }); 116 | }); 117 | }); 118 | ``` -------------------------------------------------------------------------------- /tests/data/usecases/update-file/update-file.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { beforeEach, describe, expect, test, vi } from "vitest"; 2 | import { FileRepository } from "../../../../src/data/protocols/file-repository.js"; 3 | import { ProjectRepository } from "../../../../src/data/protocols/project-repository.js"; 4 | import { UpdateFile } from "../../../../src/data/usecases/update-file/update-file.js"; 5 | import { UpdateFileParams } from "../../../../src/domain/usecases/update-file.js"; 6 | import { 7 | MockFileRepository, 8 | MockProjectRepository, 9 | } from "../../mocks/index.js"; 10 | 11 | describe("UpdateFile UseCase", () => { 12 | let sut: UpdateFile; 13 | let fileRepositoryStub: FileRepository; 14 | let projectRepositoryStub: ProjectRepository; 15 | 16 | beforeEach(() => { 17 | fileRepositoryStub = new MockFileRepository(); 18 | projectRepositoryStub = new MockProjectRepository(); 19 | sut = new UpdateFile(fileRepositoryStub, projectRepositoryStub); 20 | }); 21 | 22 | test("should call ProjectRepository.projectExists with correct projectName", async () => { 23 | const projectExistsSpy = vi.spyOn(projectRepositoryStub, "projectExists"); 24 | const params: UpdateFileParams = { 25 | projectName: "project-1", 26 | fileName: "file1.md", 27 | content: "Updated content", 28 | }; 29 | 30 | await sut.updateFile(params); 31 | 32 | expect(projectExistsSpy).toHaveBeenCalledWith("project-1"); 33 | }); 34 | 35 | test("should return null if project does not exist", async () => { 36 | vi.spyOn(projectRepositoryStub, "projectExists").mockResolvedValueOnce( 37 | false 38 | ); 39 | const params: UpdateFileParams = { 40 | projectName: "non-existent-project", 41 | fileName: "file1.md", 42 | content: "Updated content", 43 | }; 44 | 45 | const result = await sut.updateFile(params); 46 | 47 | expect(result).toBeNull(); 48 | }); 49 | 50 | test("should check if file exists before updating", async () => { 51 | const loadFileSpy = vi.spyOn(fileRepositoryStub, "loadFile"); 52 | const params: UpdateFileParams = { 53 | projectName: "project-1", 54 | fileName: "file1.md", 55 | content: "Updated content", 56 | }; 57 | 58 | await sut.updateFile(params); 59 | 60 | expect(loadFileSpy).toHaveBeenCalledWith("project-1", "file1.md"); 61 | }); 62 | 63 | test("should return null if file does not exist", async () => { 64 | vi.spyOn(fileRepositoryStub, "loadFile").mockResolvedValueOnce(null); 65 | const params: UpdateFileParams = { 66 | projectName: "project-1", 67 | fileName: "non-existent-file.md", 68 | content: "Updated content", 69 | }; 70 | 71 | const result = await sut.updateFile(params); 72 | 73 | expect(result).toBeNull(); 74 | }); 75 | 76 | test("should call FileRepository.updateFile with correct params if file exists", async () => { 77 | const updateFileSpy = vi.spyOn(fileRepositoryStub, "updateFile"); 78 | const params: UpdateFileParams = { 79 | projectName: "project-1", 80 | fileName: "file1.md", 81 | content: "Updated content", 82 | }; 83 | 84 | await sut.updateFile(params); 85 | 86 | expect(updateFileSpy).toHaveBeenCalledWith( 87 | "project-1", 88 | "file1.md", 89 | "Updated content" 90 | ); 91 | }); 92 | 93 | test("should return file content on successful file update", async () => { 94 | const params: UpdateFileParams = { 95 | projectName: "project-1", 96 | fileName: "file1.md", 97 | content: "Updated content", 98 | }; 99 | 100 | const result = await sut.updateFile(params); 101 | 102 | expect(result).toBe("Updated content"); 103 | }); 104 | 105 | test("should propagate errors if repository throws", async () => { 106 | const error = new Error("Repository error"); 107 | vi.spyOn(projectRepositoryStub, "projectExists").mockRejectedValueOnce( 108 | error 109 | ); 110 | const params: UpdateFileParams = { 111 | projectName: "project-1", 112 | fileName: "file1.md", 113 | content: "Updated content", 114 | }; 115 | 116 | await expect(sut.updateFile(params)).rejects.toThrow(error); 117 | }); 118 | }); 119 | ``` -------------------------------------------------------------------------------- /tests/presentation/controllers/update/update-controller.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it, vi } from "vitest"; 2 | import { UpdateRequest } from "../../../../src/presentation/controllers/update/protocols.js"; 3 | import { UpdateController } from "../../../../src/presentation/controllers/update/update-controller.js"; 4 | import { 5 | NotFoundError, 6 | UnexpectedError, 7 | } from "../../../../src/presentation/errors/index.js"; 8 | import { makeUpdateFileUseCase, makeValidator } from "../../mocks/index.js"; 9 | 10 | const makeSut = () => { 11 | const validatorStub = makeValidator<UpdateRequest>(); 12 | const updateFileUseCaseStub = makeUpdateFileUseCase(); 13 | const sut = new UpdateController(updateFileUseCaseStub, validatorStub); 14 | return { 15 | sut, 16 | validatorStub, 17 | updateFileUseCaseStub, 18 | }; 19 | }; 20 | 21 | describe("UpdateController", () => { 22 | it("should call validator with correct values", async () => { 23 | const { sut, validatorStub } = makeSut(); 24 | const validateSpy = vi.spyOn(validatorStub, "validate"); 25 | const request = { 26 | body: { 27 | projectName: "any_project", 28 | fileName: "any_file", 29 | content: "any_content", 30 | }, 31 | }; 32 | await sut.handle(request); 33 | expect(validateSpy).toHaveBeenCalledWith(request.body); 34 | }); 35 | 36 | it("should return 400 if validator returns an error", async () => { 37 | const { sut, validatorStub } = makeSut(); 38 | vi.spyOn(validatorStub, "validate").mockReturnValueOnce( 39 | new Error("any_error") 40 | ); 41 | const request = { 42 | body: { 43 | projectName: "any_project", 44 | fileName: "any_file", 45 | content: "any_content", 46 | }, 47 | }; 48 | const response = await sut.handle(request); 49 | expect(response).toEqual({ 50 | statusCode: 400, 51 | body: new Error("any_error"), 52 | }); 53 | }); 54 | 55 | it("should call UpdateFileUseCase with correct values", async () => { 56 | const { sut, updateFileUseCaseStub } = makeSut(); 57 | const updateFileSpy = vi.spyOn(updateFileUseCaseStub, "updateFile"); 58 | const request = { 59 | body: { 60 | projectName: "any_project", 61 | fileName: "any_file", 62 | content: "any_content", 63 | }, 64 | }; 65 | await sut.handle(request); 66 | expect(updateFileSpy).toHaveBeenCalledWith({ 67 | projectName: "any_project", 68 | fileName: "any_file", 69 | content: "any_content", 70 | }); 71 | }); 72 | 73 | it("should return 404 if UpdateFileUseCase returns null", async () => { 74 | const { sut, updateFileUseCaseStub } = makeSut(); 75 | vi.spyOn(updateFileUseCaseStub, "updateFile").mockResolvedValueOnce(null); 76 | const request = { 77 | body: { 78 | projectName: "any_project", 79 | fileName: "any_file", 80 | content: "any_content", 81 | }, 82 | }; 83 | const response = await sut.handle(request); 84 | expect(response).toEqual({ 85 | statusCode: 404, 86 | body: new NotFoundError("any_file"), 87 | }); 88 | }); 89 | 90 | it("should return 500 if UpdateFileUseCase throws", async () => { 91 | const { sut, updateFileUseCaseStub } = makeSut(); 92 | vi.spyOn(updateFileUseCaseStub, "updateFile").mockRejectedValueOnce( 93 | new Error("any_error") 94 | ); 95 | const request = { 96 | body: { 97 | projectName: "any_project", 98 | fileName: "any_file", 99 | content: "any_content", 100 | }, 101 | }; 102 | const response = await sut.handle(request); 103 | expect(response).toEqual({ 104 | statusCode: 500, 105 | body: new UnexpectedError(new Error("any_error")), 106 | }); 107 | }); 108 | 109 | it("should return 200 if valid data is provided", async () => { 110 | const { sut } = makeSut(); 111 | const request = { 112 | body: { 113 | projectName: "any_project", 114 | fileName: "any_file", 115 | content: "any_content", 116 | }, 117 | }; 118 | const response = await sut.handle(request); 119 | expect(response).toEqual({ 120 | statusCode: 200, 121 | body: "File any_file updated successfully in project any_project", 122 | }); 123 | }); 124 | }); 125 | ``` -------------------------------------------------------------------------------- /tests/data/usecases/write-file/write-file.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { beforeEach, describe, expect, test, vi } from "vitest"; 2 | import { FileRepository } from "../../../../src/data/protocols/file-repository.js"; 3 | import { ProjectRepository } from "../../../../src/data/protocols/project-repository.js"; 4 | import { WriteFile } from "../../../../src/data/usecases/write-file/write-file.js"; 5 | import { WriteFileParams } from "../../../../src/domain/usecases/write-file.js"; 6 | import { 7 | MockFileRepository, 8 | MockProjectRepository, 9 | } from "../../mocks/index.js"; 10 | 11 | describe("WriteFile UseCase", () => { 12 | let sut: WriteFile; 13 | let fileRepositoryStub: FileRepository; 14 | let projectRepositoryStub: ProjectRepository; 15 | 16 | beforeEach(() => { 17 | fileRepositoryStub = new MockFileRepository(); 18 | projectRepositoryStub = new MockProjectRepository(); 19 | sut = new WriteFile(fileRepositoryStub, projectRepositoryStub); 20 | }); 21 | 22 | test("should call ProjectRepository.ensureProject with correct projectName", async () => { 23 | const ensureProjectSpy = vi.spyOn(projectRepositoryStub, "ensureProject"); 24 | const params: WriteFileParams = { 25 | projectName: "new-project", 26 | fileName: "new-file.md", 27 | content: "New content", 28 | }; 29 | 30 | vi.spyOn(fileRepositoryStub, "loadFile") 31 | .mockResolvedValueOnce(null) // First call checking if file exists 32 | .mockResolvedValueOnce("New content"); // Second call returning the saved content 33 | 34 | await sut.writeFile(params); 35 | 36 | expect(ensureProjectSpy).toHaveBeenCalledWith("new-project"); 37 | }); 38 | 39 | test("should check if file exists before writing", async () => { 40 | const loadFileSpy = vi.spyOn(fileRepositoryStub, "loadFile"); 41 | const params: WriteFileParams = { 42 | projectName: "project-1", 43 | fileName: "new-file.md", 44 | content: "New content", 45 | }; 46 | 47 | await sut.writeFile(params); 48 | 49 | expect(loadFileSpy).toHaveBeenCalledWith("project-1", "new-file.md"); 50 | }); 51 | 52 | test("should return null if file already exists", async () => { 53 | const params: WriteFileParams = { 54 | projectName: "project-1", 55 | fileName: "file1.md", 56 | content: "New content", 57 | }; 58 | 59 | const result = await sut.writeFile(params); 60 | 61 | expect(result).toBeNull(); 62 | }); 63 | 64 | test("should call FileRepository.writeFile with correct params if file does not exist", async () => { 65 | const writeFileSpy = vi.spyOn(fileRepositoryStub, "writeFile"); 66 | const params: WriteFileParams = { 67 | projectName: "project-1", 68 | fileName: "new-file.md", 69 | content: "New content", 70 | }; 71 | 72 | vi.spyOn(fileRepositoryStub, "loadFile") 73 | .mockResolvedValueOnce(null) // First call checking if file exists 74 | .mockResolvedValueOnce("New content"); // Second call returning the saved content 75 | 76 | await sut.writeFile(params); 77 | 78 | expect(writeFileSpy).toHaveBeenCalledWith( 79 | "project-1", 80 | "new-file.md", 81 | "New content" 82 | ); 83 | }); 84 | 85 | test("should return file content on successful file creation", async () => { 86 | const params: WriteFileParams = { 87 | projectName: "project-1", 88 | fileName: "new-file.md", 89 | content: "New content", 90 | }; 91 | 92 | vi.spyOn(fileRepositoryStub, "loadFile") 93 | .mockResolvedValueOnce(null) // First call checking if file exists 94 | .mockResolvedValueOnce("New content"); // Second call returning the saved content 95 | 96 | const result = await sut.writeFile(params); 97 | 98 | expect(result).toBe("New content"); 99 | }); 100 | 101 | test("should propagate errors if repository throws", async () => { 102 | const error = new Error("Repository error"); 103 | vi.spyOn(projectRepositoryStub, "ensureProject").mockRejectedValueOnce( 104 | error 105 | ); 106 | const params: WriteFileParams = { 107 | projectName: "project-1", 108 | fileName: "new-file.md", 109 | content: "New content", 110 | }; 111 | 112 | await expect(sut.writeFile(params)).rejects.toThrow(error); 113 | }); 114 | }); 115 | ``` -------------------------------------------------------------------------------- /custom-instructions.md: -------------------------------------------------------------------------------- ```markdown 1 | # Memory Bank via MCP 2 | 3 | I'm an expert engineer whose memory resets between sessions. I rely ENTIRELY on my Memory Bank, accessed via MCP tools, and MUST read ALL memory bank files before EVERY task. 4 | 5 | ## Key Commands 6 | 7 | 1. "follow your custom instructions" 8 | 9 | - Triggers Pre-Flight Validation (\*a) 10 | - Follows Memory Bank Access Pattern (\*f) 11 | - Executes appropriate Mode flow (Plan/Act) 12 | 13 | 2. "initialize memory bank" 14 | 15 | - Follows Pre-Flight Validation (\*a) 16 | - Creates new project if needed 17 | - Establishes core files structure (\*f) 18 | 19 | 3. "update memory bank" 20 | - Triggers Documentation Updates (\*d) 21 | - Performs full file re-read 22 | - Updates based on current state 23 | 24 | ## Memory Bank lyfe cycle: 25 | 26 | ```mermaid 27 | flowchart TD 28 | A[Start] --> B["Pre-Flight Validation (*a)"] 29 | B --> C{Project Exists?} 30 | C -->|Yes| D[Check Core Files] 31 | C -->|No| E[Create Project] --> H[Create Missing Files] 32 | 33 | D --> F{All Files Present?} 34 | F -->|Yes| G["Access Memory Bank (*f)"] 35 | F -->|No| H[Create Missing Files] 36 | 37 | H --> G 38 | G --> I["Plan Mode (*b)"] 39 | G --> J["Act Mode (*c)"] 40 | 41 | I --> K[List Projects] 42 | K --> L[Select Context] 43 | L --> M[Develop Strategy] 44 | 45 | J --> N[Read .clinerules] 46 | N --> O[Execute Task] 47 | O --> P["Update Documentation (*d)"] 48 | 49 | P --> Q{Update Needed?} 50 | Q -->|Patterns/Changes| R[Read All Files] 51 | Q -->|User Request| R 52 | R --> S[Update Memory Bank] 53 | 54 | S --> T["Learning Process (*e)"] 55 | T --> U[Identify Patterns] 56 | U --> V[Validate with User] 57 | V --> W[Update .clinerules] 58 | W --> X[Apply Patterns] 59 | X --> O 60 | 61 | %% Intelligence Connections 62 | W -.->|Continuous Learning| N 63 | X -.->|Informed Execution| O 64 | ``` 65 | 66 | ## Phase Index & Requirements 67 | 68 | a) **Pre-Flight Validation** 69 | 70 | - **Triggers:** Automatic before any operation 71 | - **Checks:** 72 | - Project directory existence 73 | - Core files presence (projectbrief.md, productContext.md, etc.) 74 | - Custom documentation inventory 75 | 76 | b) **Plan Mode** 77 | 78 | - **Inputs:** Filesystem/list_directory results 79 | - **Outputs:** Strategy documented in activeContext.md 80 | - **Format Rules:** Validate paths with forward slashes 81 | 82 | c) **Act Mode** 83 | 84 | - **JSON Operations:** 85 | ```json 86 | { 87 | "projectName": "project-id", 88 | "fileName": "progress.md", 89 | "content": "Escaped\\ncontent" 90 | } 91 | ``` 92 | - **Requirements:** 93 | - Use \\n for newlines 94 | - Pure JSON (no XML) 95 | - Boolean values lowercase (true/false) 96 | 97 | d) **Documentation Updates** 98 | 99 | - **Triggers:** 100 | - ≥25% code impact changes 101 | - New pattern discovery 102 | - User request "update memory bank" 103 | - Context ambiguity detected 104 | - **Process:** Full file re-read before update 105 | 106 | e) **Project Intelligence** 107 | 108 | - **.clinerules Requirements:** 109 | - Capture critical implementation paths 110 | - Document user workflow preferences 111 | - Track tool usage patterns 112 | - Record project-specific decisions 113 | - **Cycle:** Continuous validate → update → apply 114 | 115 | f) **Memory Bank Structure** 116 | 117 | ```mermaid 118 | flowchart TD 119 | PB[projectbrief.md\nCore requirements/goals] --> PC[productContext.md\nProblem context/solutions] 120 | PB --> SP[systemPatterns.md\nArchitecture/patterns] 121 | PB --> TC[techContext.md\nTech stack/setup] 122 | 123 | PC --> AC[activeContext.md\nCurrent focus/decisions] 124 | SP --> AC 125 | TC --> AC 126 | 127 | AC --> P[progress.md\nStatus/roadmap] 128 | 129 | %% Custom files section 130 | subgraph CF[Custom Files] 131 | CF1[features/*.md\nFeature specs] 132 | CF2[api/*.md\nAPI documentation] 133 | CF3[deployment/*.md\nDeployment guides] 134 | end 135 | 136 | %% Connect custom files to main structure 137 | AC -.-> CF 138 | CF -.-> P 139 | 140 | style PB fill:#e066ff,stroke:#333,stroke-width:2px 141 | style AC fill:#4d94ff,stroke:#333,stroke-width:2px 142 | style P fill:#2eb82e,stroke:#333,stroke-width:2px 143 | style CF fill:#fff,stroke:#333,stroke-width:1px,stroke-dasharray: 5 5 144 | style CF1 fill:#fff,stroke:#333 145 | style CF2 fill:#fff,stroke:#333 146 | style CF3 fill:#fff,stroke:#333 147 | ``` 148 | 149 | - **File Relationships:** 150 | - projectbrief.md feeds into all context files 151 | - All context files inform activeContext.md 152 | - progress.md tracks implementation based on active context 153 | - **Color Coding:** 154 | - Purple: Foundation documents 155 | - Blue: Active work documents 156 | - Green: Status tracking 157 | - Dashed: Custom documentation (flexible/optional) 158 | - **Access Pattern:** 159 | 160 | - Always read in hierarchical order 161 | - Update in reverse order (progress → active → others) 162 | - .clinerules accessed throughout process 163 | - Custom files integrated based on project needs 164 | 165 | - **Custom Files:** 166 | - Can be added when specific documentation needs arise 167 | - Common examples: 168 | - Feature specifications 169 | - API documentation 170 | - Integration guides 171 | - Testing strategies 172 | - Deployment procedures 173 | - Should follow main structure's naming patterns 174 | - Must be referenced in activeContext.md when added 175 | ``` -------------------------------------------------------------------------------- /tests/infra/filesystem/repositories/fs-file-repository.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import fs from "fs-extra"; 2 | import os from "os"; 3 | import path from "path"; 4 | import { afterEach, beforeEach, describe, expect, it } from "vitest"; 5 | import { FsFileRepository } from "../../../../src/infra/filesystem/repositories/fs-file-repository.js"; 6 | 7 | describe("FsFileRepository", () => { 8 | let tempDir: string; 9 | let repository: FsFileRepository; 10 | const projectName = "test-project"; 11 | const fileName = "test.md"; 12 | const fileContent = "Test content"; 13 | 14 | beforeEach(async () => { 15 | // Create a temporary directory for tests 16 | tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "memory-bank-test-")); 17 | repository = new FsFileRepository(tempDir); 18 | 19 | // Create a test project directory 20 | await fs.mkdir(path.join(tempDir, projectName)); 21 | }); 22 | 23 | afterEach(() => { 24 | // Clean up after tests 25 | fs.removeSync(tempDir); 26 | }); 27 | 28 | describe("listFiles", () => { 29 | it("should return an empty array for a project that doesn't exist", async () => { 30 | const result = await repository.listFiles("non-existent-project"); 31 | expect(result).toEqual([]); 32 | }); 33 | 34 | it("should return an empty array when no files exist in the project", async () => { 35 | const result = await repository.listFiles(projectName); 36 | expect(result).toEqual([]); 37 | }); 38 | 39 | it("should return file names within the project directory", async () => { 40 | // Create test files 41 | await fs.writeFile(path.join(tempDir, projectName, "file1.md"), "test"); 42 | await fs.writeFile(path.join(tempDir, projectName, "file2.txt"), "test"); 43 | // Create a directory to ensure it's not returned 44 | await fs.mkdir(path.join(tempDir, projectName, "not-a-file")); 45 | 46 | const result = await repository.listFiles(projectName); 47 | 48 | expect(result).toHaveLength(2); 49 | expect(result).toEqual(expect.arrayContaining(["file1.md", "file2.txt"])); 50 | }); 51 | }); 52 | 53 | describe("loadFile", () => { 54 | it("should return null when the file doesn't exist", async () => { 55 | const result = await repository.loadFile(projectName, "non-existent.md"); 56 | expect(result).toBeNull(); 57 | }); 58 | 59 | it("should return null when the project doesn't exist", async () => { 60 | const result = await repository.loadFile( 61 | "non-existent-project", 62 | fileName 63 | ); 64 | expect(result).toBeNull(); 65 | }); 66 | 67 | it("should return the file content when the file exists", async () => { 68 | // Create a test file 69 | await fs.writeFile( 70 | path.join(tempDir, projectName, fileName), 71 | fileContent 72 | ); 73 | 74 | const result = await repository.loadFile(projectName, fileName); 75 | 76 | expect(result).toBe(fileContent); 77 | }); 78 | }); 79 | 80 | describe("writeFile", () => { 81 | it("should create the project directory if it doesn't exist", async () => { 82 | const newProjectName = "new-project"; 83 | const newFilePath = path.join(tempDir, newProjectName, fileName); 84 | 85 | await repository.writeFile(newProjectName, fileName, fileContent); 86 | 87 | const exists = await fs.pathExists(newFilePath); 88 | expect(exists).toBe(true); 89 | }); 90 | 91 | it("should write file content to the specified file", async () => { 92 | await repository.writeFile(projectName, fileName, fileContent); 93 | 94 | const content = await fs.readFile( 95 | path.join(tempDir, projectName, fileName), 96 | "utf-8" 97 | ); 98 | expect(content).toBe(fileContent); 99 | }); 100 | 101 | it("should return the file content after writing", async () => { 102 | const result = await repository.writeFile( 103 | projectName, 104 | fileName, 105 | fileContent 106 | ); 107 | 108 | expect(result).toBe(fileContent); 109 | }); 110 | 111 | it("should return null if the file already exists", async () => { 112 | // Create a test file first 113 | await fs.writeFile( 114 | path.join(tempDir, projectName, fileName), 115 | "Original content" 116 | ); 117 | 118 | const result = await repository.writeFile( 119 | projectName, 120 | fileName, 121 | fileContent 122 | ); 123 | 124 | expect(result).toBeNull(); 125 | 126 | // Verify content wasn't changed 127 | const content = await fs.readFile( 128 | path.join(tempDir, projectName, fileName), 129 | "utf-8" 130 | ); 131 | expect(content).toBe("Original content"); 132 | }); 133 | }); 134 | 135 | describe("updateFile", () => { 136 | it("should return null when the file doesn't exist", async () => { 137 | const result = await repository.updateFile( 138 | projectName, 139 | "non-existent.md", 140 | fileContent 141 | ); 142 | expect(result).toBeNull(); 143 | }); 144 | 145 | it("should return null when the project doesn't exist", async () => { 146 | const result = await repository.updateFile( 147 | "non-existent-project", 148 | fileName, 149 | fileContent 150 | ); 151 | expect(result).toBeNull(); 152 | }); 153 | 154 | it("should update file content for an existing file", async () => { 155 | // Create a test file first 156 | await fs.writeFile( 157 | path.join(tempDir, projectName, fileName), 158 | "Original content" 159 | ); 160 | 161 | const updatedContent = "Updated content"; 162 | const result = await repository.updateFile( 163 | projectName, 164 | fileName, 165 | updatedContent 166 | ); 167 | 168 | expect(result).toBe(updatedContent); 169 | 170 | // Verify content was changed 171 | const content = await fs.readFile( 172 | path.join(tempDir, projectName, fileName), 173 | "utf-8" 174 | ); 175 | expect(content).toBe(updatedContent); 176 | }); 177 | 178 | it("should return the updated file content", async () => { 179 | // Create a test file first 180 | await fs.writeFile( 181 | path.join(tempDir, projectName, fileName), 182 | "Original content" 183 | ); 184 | 185 | const updatedContent = "Updated content"; 186 | const result = await repository.updateFile( 187 | projectName, 188 | fileName, 189 | updatedContent 190 | ); 191 | 192 | expect(result).toBe(updatedContent); 193 | }); 194 | }); 195 | }); 196 | ```