├── .github └── workflows │ └── release.yml ├── .gitignore ├── .uv └── config.toml ├── CHANGELOG.md ├── CLAUDE.md ├── Dockerfile ├── README.md ├── claude_config ├── Claude Desktop Config for Apple Silicon Macs.json └── claude_desktop_config.json ├── claude_desktop_config_template.json ├── cloudflare_worker ├── package-lock.json ├── package.json ├── src │ └── index.ts ├── tsconfig.json └── wrangler.example.toml ├── docker-compose.pythonpath.yml ├── docker-compose.uv.yml ├── docker-compose.yml ├── docs ├── MCP_Cloudflare_Design.md ├── README.md ├── guides │ ├── claude-code-integration.md │ ├── docker.md │ ├── installation.md │ ├── invocation_guide.md │ ├── migration.md │ ├── scripts.md │ ├── troubleshooting.md │ └── windows-setup.md ├── integrations.md └── technical │ ├── development.md │ ├── docker-deployment.md │ ├── memory-migration.md │ └── tag-storage.md ├── install.py ├── license.md ├── mcpServers.example.json ├── mcp_config.json ├── memory_wrapper.py ├── memory_wrapper_uv.py ├── pyproject.toml ├── requirements.txt ├── requirements_current.txt ├── run-with-uv.bat ├── run-with-uv.sh ├── scripts ├── TIME_BASED_RECALL_FIX.md ├── backup_memories.py ├── cleanup_memories.md ├── cleanup_memories.py ├── convert_to_uv.py ├── debug_dependencies.py ├── fix_pytorch_dependency.py ├── fix_readline.py ├── fix_sitecustomize.py ├── install_uv.py ├── install_windows.py ├── list-collections.py ├── mcp-migration.py ├── merge_database_incomplete.py ├── migrate_tags.py ├── migrate_timestamps.py ├── repair_memories.py ├── requirements-migration.txt ├── restore_memories.py ├── run_memory_server.py ├── run_migration.bat ├── run_migration.sh ├── test-connection.py ├── test_installation.py ├── validate_memories.py ├── verify_environment.py ├── verify_pytorch_windows.py └── verify_torch.py ├── setup.py ├── smithery.yaml ├── src ├── chroma_test_isolated.py ├── mcp_memory_service │ ├── __init__.py │ ├── config.py │ ├── models │ │ ├── __init__.py │ │ └── memory.py │ ├── server.py │ ├── storage │ │ ├── __init__.py │ │ ├── base.py │ │ └── chroma.py │ └── utils │ │ ├── __init__.py │ │ ├── db_utils.py │ │ ├── debug.py │ │ ├── hashing.py │ │ ├── system_detection.py │ │ ├── time_parser.py │ │ └── utils.py ├── test_client.py └── test_management.py ├── templates └── default.md.j2 ├── test_client.py ├── tests ├── __init__.py ├── test_database.py ├── test_memory_ops.py ├── test_semantic_search.py ├── test_tag_storage.py └── test_time_parser.py ├── uv.lock ├── uv_wrapper.py └── vibe-tools.md /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | concurrency: release 12 | permissions: 13 | id-token: write 14 | contents: write 15 | 16 | steps: 17 | - uses: actions/checkout@v3 # would probably be better to use v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: '3.9' # this setup python action uses a separate version than the python-semantic-release, thats why we had the error 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | python -m pip install build hatchling 30 | 31 | - name: Verify build module installation 32 | run: python -m pip show build 33 | 34 | - name: Build package 35 | run: python -m build 36 | 37 | - name: Python Semantic Release 38 | uses: python-semantic-release/python-semantic-release@master 39 | with: 40 | github_token: ${{ secrets.GITHUB_TOKEN }} 41 | root_options: "-v --pypi-build-dependencies build,hatchling" 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Aider files 2 | .aider.* 3 | 4 | # Python 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | *.so 9 | 10 | # Python environment & build artifacts 11 | .Python 12 | env/ 13 | .venv/ 14 | venv/ 15 | py310_venv/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # Python project tools 33 | pip-wheel-metadata/ 34 | __pypackages__/ 35 | 36 | # Virtual Environment activation files (redundant due to .venv/ but explicit if needed) 37 | .env 38 | .env.* 39 | 40 | # IDEs & Editors 41 | .idea/ 42 | .vscode/ 43 | *.swp 44 | *.swo 45 | 46 | # OS-specific files 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # CodeGPT / Extensions 52 | .codegpt/ 53 | 54 | # ChromaDB artifacts 55 | chroma_db/ 56 | tests/test_db/chroma.sqlite3 57 | 58 | # Project-specific artifacts 59 | backups/ 60 | test_output.txt 61 | dxdiag_output.txt 62 | dxdiag.txt 63 | claude_desktop_config_updated.json 64 | claude_config/claude_desktop_config.json 65 | 66 | # Remove these if mistakenly included 67 | =1.0.0, 68 | =11.0.3 69 | 70 | # Logs and debugging 71 | *.log 72 | *.bak 73 | *.tmp 74 | *.old 75 | 76 | # Optional: VSCode debugging & Python caches 77 | *.coverage 78 | coverage.* 79 | .cache/ 80 | .pytest_cache/ 81 | .tox/ 82 | nosetests.xml 83 | coverage.xml 84 | *.cover 85 | .hypothesis/ 86 | -------------------------------------------------------------------------------- /.uv/config.toml: -------------------------------------------------------------------------------- 1 | [tool.uv] 2 | # Default resolver settings 3 | resolver = "strict" 4 | python-version = ">=3.10" 5 | 6 | # PyTorch settings by platform 7 | [tool.uv.platform.windows] 8 | extra-index-url = ["https://download.pytorch.org/whl/cu118"] 9 | 10 | [tool.uv.platform.darwin] 11 | requirements-file = "requirements_macos.txt" 12 | 13 | [tool.uv.platform.linux] 14 | requirements-file = "requirements_linux.txt" 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | 4 | ## v0.1.0 (2024-12-27) 5 | 6 | ### Chores 7 | 8 | - Update gitignore 9 | ([`97ba25c`](https://github.com/doobidoo/mcp-memory-service/commit/97ba25c83113ed228d6684b8c65bc65774c0b704)) 10 | 11 | ### Features 12 | 13 | - Add MCP protocol compliance and fix response formats 14 | ([`fefd579`](https://github.com/doobidoo/mcp-memory-service/commit/fefd5796b3fb758023bb574b508940a651e48ad5)) 15 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # MCP Memory Service - Development Guidelines 2 | 3 | ## Commands 4 | - Run memory server: `python scripts/run_memory_server.py` 5 | - Run tests: `pytest tests/` 6 | - Run specific test: `pytest tests/test_memory_ops.py::test_store_memory -v` 7 | - Check environment: `python scripts/verify_environment_enhanced.py` 8 | - Windows installation: `python scripts/install_windows.py` 9 | - Build package: `python -m build` 10 | 11 | ## Installation Guidelines 12 | - Always install in a virtual environment: `python -m venv venv` 13 | - Use `install.py` for cross-platform installation 14 | - Windows requires special PyTorch installation with correct index URL: 15 | ```bash 16 | pip install torch==2.1.0 torchvision==2.1.0 torchaudio==2.1.0 --index-url https://download.pytorch.org/whl/cu118 17 | ``` 18 | - For recursion errors, run: `python scripts/fix_sitecustomize.py` 19 | 20 | ## Memory Service Invocation 21 | - See the comprehensive [Invocation Guide](docs/guides/invocation_guide.md) for full details 22 | - Key trigger phrases: 23 | - **Storage**: "remember that", "remember this", "save to memory", "store in memory" 24 | - **Retrieval**: "do you remember", "recall", "retrieve from memory", "search your memory for" 25 | - **Tag-based**: "find memories with tag", "search for tag", "retrieve memories tagged" 26 | - **Deletion**: "forget", "delete from memory", "remove from memory" 27 | 28 | ## Code Style 29 | - Python 3.10+ with type hints 30 | - Use dataclasses for models (see `models/memory.py`) 31 | - Triple-quoted docstrings for modules and functions 32 | - Async/await pattern for all I/O operations 33 | - Error handling with specific exception types and informative messages 34 | - Logging with appropriate levels for different severity 35 | - Commit messages follow semantic release format: `type(scope): message` 36 | 37 | ## Project Structure 38 | - `src/mcp_memory_service/` - Core package code 39 | - `models/` - Data models 40 | - `storage/` - Database abstraction 41 | - `utils/` - Helper functions 42 | - `server.py` - MCP protocol implementation 43 | - `scripts/` - Utility scripts 44 | - `memory_wrapper.py` - Windows wrapper script 45 | - `install.py` - Cross-platform installation script 46 | 47 | ## Dependencies 48 | - ChromaDB (0.5.23) for vector database 49 | - sentence-transformers (>=2.2.2) for embeddings 50 | - PyTorch (platform-specific installation) 51 | - MCP protocol (>=1.0.0, <2.0.0) for client-server communication 52 | 53 | ## Troubleshooting 54 | - For Windows installation issues, use `scripts/install_windows.py` 55 | - Apple Silicon requires Python 3.10+ built for ARM64 56 | - CUDA issues: verify with `torch.cuda.is_available()` 57 | - For MCP protocol issues, check `server.py` for required methods -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Platform-agnostic Docker support with UV integration 2 | FROM python:3.10-slim 3 | 4 | # Set environment variables 5 | ENV PYTHONUNBUFFERED=1 \ 6 | MCP_MEMORY_CHROMA_PATH=/app/chroma_db \ 7 | MCP_MEMORY_BACKUPS_PATH=/app/backups \ 8 | PYTHONPATH=/app 9 | 10 | # Set the working directory 11 | WORKDIR /app 12 | 13 | # Install system dependencies 14 | RUN apt-get update && \ 15 | apt-get install -y --no-install-recommends \ 16 | build-essential \ 17 | gcc \ 18 | g++ \ 19 | && rm -rf /var/lib/apt/lists/* 20 | 21 | # Copy essential files 22 | COPY requirements.txt . 23 | COPY setup.py . 24 | COPY pyproject.toml . 25 | COPY uv.lock . 26 | COPY README.md . 27 | COPY scripts/install_uv.py . 28 | 29 | # Install UV 30 | RUN python install_uv.py 31 | 32 | # Create directories for data persistence 33 | RUN mkdir -p /app/chroma_db /app/backups 34 | 35 | # Copy source code 36 | COPY src/ /app/src/ 37 | COPY uv_wrapper.py memory_wrapper_uv.py ./ 38 | 39 | # Install the package with UV 40 | RUN python -m uv pip install -e . 41 | 42 | # Configure stdio for MCP communication 43 | RUN chmod a+rw /dev/stdin /dev/stdout /dev/stderr 44 | 45 | # Add volume mount points for data persistence 46 | VOLUME ["/app/chroma_db", "/app/backups"] 47 | 48 | # Expose the port (if needed) 49 | EXPOSE 8000 50 | 51 | # Run the memory service using UV 52 | ENTRYPOINT ["python", "-u", "uv_wrapper.py"] 53 | -------------------------------------------------------------------------------- /claude_config/Claude Desktop Config for Apple Silicon Macs.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "memory": { 4 | "command": "PATH_TO_YOUR_CLONED_PROJECT/.venv/bin/uv", 5 | "args": [ 6 | "--directory", 7 | "PATH_TO_YOUR_CLONED_PROJECT", 8 | "run", 9 | "memory" 10 | ], 11 | "env": { 12 | "MCP_MEMORY_CHROMA_PATH": "/Users/Your_Username/Library/Application Support/mcp-memory/chroma_db", 13 | "MCP_MEMORY_BACKUPS_PATH": "/Users/Your_Username/Library/Application Support/mcp-memory/backups" 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /claude_config/claude_desktop_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "memory": { 4 | "command": "uv", 5 | "args": [ 6 | "--directory", 7 | "/workspaces/mcp-memory-service", 8 | "run", 9 | "memory" 10 | ], 11 | "env": { 12 | "MCP_MEMORY_CHROMA_PATH": "/home/codespace/.local/share/mcp-memory/chroma_db", 13 | "MCP_MEMORY_BACKUPS_PATH": "/home/codespace/.local/share/mcp-memory/backups" 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /claude_desktop_config_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "memory": { 4 | "command": "${PYTHON_PATH}", 5 | "args": [ 6 | "${PROJECT_PATH}/scripts/run_memory_server.py" 7 | ], 8 | "env": { 9 | "MCP_MEMORY_CHROMA_PATH": "${USER_DATA_PATH}/mcp-memory/chroma_db", 10 | "MCP_MEMORY_BACKUPS_PATH": "${USER_DATA_PATH}/mcp-memory/backups", 11 | "PYTHONNOUSERSITE": "1", 12 | "PIP_NO_DEPENDENCIES": "1", 13 | "PIP_NO_INSTALL": "1", 14 | "PYTORCH_ENABLE_MPS_FALLBACK": "1", 15 | "PYTORCH_CUDA_ALLOC_CONF": "max_split_size_mb:128" 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /cloudflare_worker/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudflare_worker", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "typescript": "^5.8.3" 9 | }, 10 | "devDependencies": { 11 | "@cloudflare/workers-types": "^4.20250409.0" 12 | } 13 | }, 14 | "node_modules/@cloudflare/workers-types": { 15 | "version": "4.20250409.0", 16 | "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250409.0.tgz", 17 | "integrity": "sha512-yPxxwE5nr168huEfLNOB6904OsvIWcq0tWT23NMD6jT5SIp2ds3oOGANw7wz39r5y3jZYC2h1OnGwnZXJDDCOg==", 18 | "dev": true, 19 | "license": "MIT OR Apache-2.0" 20 | }, 21 | "node_modules/typescript": { 22 | "version": "5.8.3", 23 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 24 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 25 | "license": "Apache-2.0", 26 | "bin": { 27 | "tsc": "bin/tsc", 28 | "tsserver": "bin/tsserver" 29 | }, 30 | "engines": { 31 | "node": ">=14.17" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cloudflare_worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@cloudflare/workers-types": "^4.20250409.0" 4 | }, 5 | "dependencies": { 6 | "typescript": "^5.8.3" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /cloudflare_worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2021", 4 | "lib": ["es2021"], 5 | "module": "es2022", 6 | "moduleResolution": "node", 7 | "types": ["@cloudflare/workers-types"], 8 | "noEmit": true, 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true 12 | }, 13 | "include": ["src/**/*"] 14 | } -------------------------------------------------------------------------------- /cloudflare_worker/wrangler.example.toml: -------------------------------------------------------------------------------- 1 | name = "mcp-memory-cloudflare" 2 | main = "src/index.ts" 3 | compatibility_date = "2025-04-09" 4 | 5 | [[d1_databases]] 6 | binding = "DB" 7 | database_id = "YOUR_DATABASE_ID_HERE" 8 | database_name = "mcp_memory_service" 9 | 10 | [ai] 11 | binding = "AI" 12 | 13 | [observability] 14 | enabled = true 15 | head_sampling_rate = 1 # optional. default = 1. 16 | 17 | # Environment variables 18 | [vars] 19 | DEBUG = "0" # Set to "1" to enable debug output 20 | -------------------------------------------------------------------------------- /docker-compose.pythonpath.yml: -------------------------------------------------------------------------------- 1 | services: 2 | memory-service: 3 | image: python:3.10-slim 4 | working_dir: /app 5 | ports: 6 | - "8000:8000" 7 | volumes: 8 | - .:/app 9 | - ${CHROMA_DB_PATH:-$HOME/mcp-memory/chroma_db}:/app/chroma_db 10 | - ${BACKUPS_PATH:-$HOME/mcp-memory/backups}:/app/backups 11 | environment: 12 | - MCP_MEMORY_CHROMA_PATH=/app/chroma_db 13 | - MCP_MEMORY_BACKUPS_PATH=/app/backups 14 | - LOG_LEVEL=INFO 15 | - MAX_RESULTS_PER_QUERY=10 16 | - SIMILARITY_THRESHOLD=0.7 17 | - PYTHONPATH=/app/src:/app 18 | - PYTHONUNBUFFERED=1 19 | restart: unless-stopped 20 | command: > 21 | bash -c " 22 | apt-get update && 23 | apt-get install -y --no-install-recommends build-essential gcc g++ && 24 | rm -rf /var/lib/apt/lists/* && 25 | pip install -r requirements.txt && 26 | chmod a+rw /dev/stdin /dev/stdout /dev/stderr && 27 | python -u -m mcp_memory_service.server 28 | " 29 | -------------------------------------------------------------------------------- /docker-compose.uv.yml: -------------------------------------------------------------------------------- 1 | services: 2 | memory-service: 3 | image: python:3.10-slim 4 | working_dir: /app 5 | ports: 6 | - "8000:8000" 7 | volumes: 8 | - .:/app 9 | - ${CHROMA_DB_PATH:-$HOME/mcp-memory/chroma_db}:/app/chroma_db 10 | - ${BACKUPS_PATH:-$HOME/mcp-memory/backups}:/app/backups 11 | environment: 12 | - MCP_MEMORY_CHROMA_PATH=/app/chroma_db 13 | - MCP_MEMORY_BACKUPS_PATH=/app/backups 14 | - LOG_LEVEL=INFO 15 | - MAX_RESULTS_PER_QUERY=10 16 | - SIMILARITY_THRESHOLD=0.7 17 | - PYTHONPATH=/app 18 | - PYTHONUNBUFFERED=1 19 | - UV_ACTIVE=1 20 | restart: unless-stopped 21 | command: > 22 | bash -c " 23 | apt-get update && 24 | apt-get install -y --no-install-recommends build-essential gcc g++ && 25 | rm -rf /var/lib/apt/lists/* && 26 | pip install uv && 27 | chmod a+rw /dev/stdin /dev/stdout /dev/stderr && 28 | python -u uv_wrapper.py 29 | " 30 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | memory-service: 3 | image: python:3.10-slim 4 | working_dir: /app 5 | ports: 6 | - "8000:8000" 7 | volumes: 8 | - .:/app 9 | - ${CHROMA_DB_PATH:-$HOME/mcp-memory/chroma_db}:/app/chroma_db 10 | - ${BACKUPS_PATH:-$HOME/mcp-memory/backups}:/app/backups 11 | environment: 12 | - MCP_MEMORY_CHROMA_PATH=/app/chroma_db 13 | - MCP_MEMORY_BACKUPS_PATH=/app/backups 14 | - LOG_LEVEL=INFO 15 | - MAX_RESULTS_PER_QUERY=10 16 | - SIMILARITY_THRESHOLD=0.7 17 | - PYTHONPATH=/app 18 | - PYTHONUNBUFFERED=1 19 | restart: unless-stopped 20 | command: > 21 | bash -c " 22 | apt-get update && 23 | apt-get install -y --no-install-recommends build-essential gcc g++ && 24 | rm -rf /var/lib/apt/lists/* && 25 | pip install -e . && 26 | chmod a+rw /dev/stdin /dev/stdout /dev/stderr && 27 | python -u -m mcp_memory_service.server 28 | " -------------------------------------------------------------------------------- /docs/MCP_Cloudflare_Design.md: -------------------------------------------------------------------------------- 1 | # MCP Memory Service - Cloudflare Migration & Redesign Plan 2 | 3 | ## Goals 4 | - Replace local ChromaDB + Huggingface with Cloudflare services 5 | - Fully remote, serverless, scalable, accessible via SSE MCP protocol 6 | - Support semantic search, tagging, time-based recall, backups 7 | - Prepare for future security (Zero Trust) 8 | 9 | --- 10 | 11 | ## Cloudflare Services to Use 12 | 13 | | Functionality | Current (Local) | Cloudflare Replacement | 14 | |--------------------------|-------------------------------------|----------------------------------------------------------| 15 | | Vector storage & search | ChromaDB | D1 (SQLite) + custom similarity search in Worker | 16 | | Embedding generation | Huggingface sentence-transformers | Workers AI | 17 | | Metadata & tags | ChromaDB metadata | D1 (JSON columns) | 18 | | Backups | File copy of ChromaDB | Export D1 data to R2 Object Storage | 19 | | API / SSE server | Local Python MCP server | Cloudflare Worker | 20 | 21 | --- 22 | 23 | ## Architecture Overview 24 | 25 | ```mermaid 26 | flowchart TD 27 | subgraph Client 28 | A[MCP Client] 29 | end 30 | 31 | subgraph Cloudflare 32 | B[Cloudflare Worker (MCP Server)] 33 | C[D1 Database (Vectors + Metadata)] 34 | D[Workers AI (Embeddings)] 35 | E[R2 Storage (Backups)] 36 | end 37 | 38 | A -- SSE --> B 39 | B -- SQL --> C 40 | B -- API Call --> D 41 | B -- Backup Export --> E 42 | ``` 43 | 44 | --- 45 | 46 | ## Design Summary 47 | 48 | - **Store Memory:** 49 | - Client sends content + metadata 50 | - Worker calls Workers AI to generate embedding 51 | - Worker stores embedding + metadata in D1 (as JSON or BLOB) 52 | - **Retrieve Memory:** 53 | - Client sends query 54 | - Worker calls Workers AI to embed query 55 | - Worker fetches candidate vectors from D1 (all or filtered by tags/time) 56 | - Worker computes cosine similarity, returns top-N matches 57 | - **Tagging:** 58 | - Tags stored as JSON/text in D1 rows 59 | - **Backups:** 60 | - Periodic export of D1 data to R2 61 | - Optionally triggered via API call or scheduled Worker 62 | - **Security:** 63 | - Design API to support future Zero Trust integration 64 | 65 | --- 66 | 67 | ## Key Operations Mapping 68 | 69 | | Operation | How it works on Cloudflare | 70 | |-------------------------|-------------------------------------------------------------------------| 71 | | Store Memory | Generate embedding via Workers AI, store in D1 with metadata | 72 | | Retrieve Memory | Embed query via Workers AI, fetch candidates, compute similarity | 73 | | Search by Tag | SQL filter in D1, then similarity search | 74 | | Recall by Time | SQL filter by timestamp, then similarity search | 75 | | Exact Match | SQL query by content hash | 76 | | Backups | Export D1 data to R2 | 77 | 78 | --- 79 | 80 | ## Next Steps 81 | 82 | 1. **Design D1 schema** for vectors + metadata 83 | 2. **Design Worker API** (MCP SSE endpoints) 84 | 3. **Prototype embedding call** to Workers AI 85 | 4. **Implement Worker logic** for store/retrieve 86 | 5. **Set up D1 and R2** in Cloudflare account 87 | 6. **Deploy Worker** 88 | 7. **Test via curl** 89 | 8. **Notify progress via mcp-notifications** 90 | 91 | --- 92 | 93 | ## Security Considerations 94 | 95 | - Plan to integrate Cloudflare Zero Trust for authentication and access control 96 | - Use API tokens or mTLS in future 97 | - Design endpoints to be easily securable 98 | 99 | --- 100 | 101 | ## Summary 102 | 103 | This plan migrates the MCP Memory Service to a fully serverless, scalable, Cloudflare-native architecture, replacing local dependencies with managed services, and prepares for future security enhancements. -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # MCP Memory Service Documentation 2 | 3 | Welcome to the MCP Memory Service documentation. This directory contains comprehensive guides for installing, using, and troubleshooting the service. 4 | 5 | ## Guides 6 | 7 | - [Installation Guide](guides/installation.md) - Comprehensive installation instructions for all platforms 8 | - [Troubleshooting Guide](guides/troubleshooting.md) - Solutions for common issues and debugging procedures 9 | - [Migration Guide](guides/migration.md) - Instructions for migrating memories between different ChromaDB instances 10 | 11 | ## Technical Documentation 12 | 13 | - [Tag Storage Procedure](technical/tag-storage.md) - Technical details about tag storage and migration 14 | - [Memory Migration](technical/memory-migration.md) - Technical details about memory migration process 15 | 16 | ## Quick Links 17 | 18 | - [Main README](../README.md) - Overview of the service and its features 19 | - [Hardware Compatibility](../README.md#hardware-compatibility) - Supported platforms and accelerators 20 | - [Configuration Options](../README.md#configuration-options) - Available environment variables and settings 21 | 22 | ## Platform-Specific Notes 23 | 24 | ### Windows 25 | - Uses Windows-specific installation script 26 | - Requires PyTorch wheels from specific index URL 27 | - See [Windows Installation Guide](guides/installation.md#windows) 28 | 29 | ### macOS 30 | - Intel x86_64: Uses specific PyTorch versions for compatibility 31 | - Apple Silicon: Supports MPS acceleration with fallbacks 32 | - See [macOS Installation Guide](guides/installation.md#macos) 33 | 34 | ## Available Scripts 35 | 36 | The `scripts/` directory contains several useful tools: 37 | 38 | ### Core Scripts 39 | - `run_memory_server.py` - Direct runner for MCP Memory Service 40 | - `verify_environment.py` - Enhanced environment compatibility verification 41 | - `fix_sitecustomize.py` - Fix for recursion issues with enhanced platform support 42 | - `mcp-migration.py` - Memory migration tool supporting local and remote migrations 43 | 44 | ### Platform-Specific Scripts 45 | - `install_windows.py` - Windows-specific installation 46 | - `verify_pytorch_windows.py` - Windows PyTorch verification 47 | 48 | ## Script Usage Examples 49 | 50 | ### Environment Verification 51 | ```bash 52 | python scripts/verify_environment.py 53 | ``` 54 | 55 | ### Memory Migration 56 | ```bash 57 | # Local to Remote Migration 58 | python scripts/mcp-migration.py --source-type local --source-config /path/to/local/chroma --target-type remote --target-config '{"host": "remote-host", "port": 8000}' 59 | 60 | # Remote to Local Migration 61 | python scripts/mcp-migration.py --source-type remote --source-config '{"host": "remote-host", "port": 8000}' --target-type local --target-config /path/to/local/chroma 62 | ``` 63 | 64 | ### Site Customization Fix 65 | ```bash 66 | python scripts/fix_sitecustomize.py 67 | ``` 68 | 69 | ## Configuration 70 | 71 | - See [Claude MCP Configuration](../README.md#claude-mcp-configuration) for configuration options 72 | - Sample configuration templates are available in the `claude_config/` directory -------------------------------------------------------------------------------- /docs/guides/claude-code-integration.md: -------------------------------------------------------------------------------- 1 | # Using MCP Memory Service with Claude Code 2 | 3 | This guide explains how to integrate the MCP Memory Service with Claude Code, allowing you to use persistent memory capabilities in the Claude CLI environment. 4 | 5 | ## Prerequisites 6 | 7 | Before you begin, ensure you have: 8 | 9 | 1. Installed [Claude Code](https://www.anthropic.com/news/introducing-claude-code) CLI tool 10 | 2. Set up the MCP Memory Service on your machine 11 | 3. Basic familiarity with command-line interfaces 12 | 13 | ## Registering the Memory Service with Claude Code 14 | 15 | You can register the MCP Memory Service to work with Claude Code using the `claude mcp add` command. 16 | 17 | ### Check Existing MCP Servers 18 | 19 | To see which MCP servers are already registered with Claude: 20 | 21 | ```bash 22 | claude mcp list 23 | ``` 24 | 25 | ### Add the Memory Service 26 | 27 | To add the memory service that's running on your local machine: 28 | 29 | ```bash 30 | claude mcp add memory-service spawn -- /path/to/your/command 31 | ``` 32 | 33 | For example, if you've installed the memory service using UV (recommended): 34 | 35 | ```bash 36 | claude mcp add memory-service spawn -- /opt/homebrew/bin/uv --directory /Users/yourusername/path/to/mcp-memory-service run memory 37 | ``` 38 | 39 | Replace the path elements with the actual paths on your system. 40 | 41 | ## Example Configuration 42 | 43 | Here's a real-world example of adding the memory service to Claude Code: 44 | 45 | ```bash 46 | claude mcp add memory-service spawn -- /opt/homebrew/bin/uv --directory /Users/yourusername/Documents/GitHub/mcp-memory-service run memory 47 | ``` 48 | 49 | This command: 50 | 1. Registers a new MCP server named "memory-service" 51 | 2. Uses the "spawn" transport method, which runs the command when needed 52 | 3. Specifies the full path to the UV command 53 | 4. Sets the working directory to your mcp-memory-service location 54 | 5. Runs the "memory" module 55 | 56 | After running this command, you should see a confirmation message like: 57 | 58 | ``` 59 | Added stdio MCP server memory-service with command: spawn /opt/homebrew/bin/uv --directory /Users/yourusername/Documents/GitHub/mcp-memory-service run memory to local config 60 | ``` 61 | 62 | ## Using Memory Functions in Claude Code 63 | 64 | Once registered, you can use the memory service directly in your conversations with Claude Code. The memory functions available include: 65 | 66 | - Storing memories 67 | - Retrieving memories based on semantic search 68 | - Recalling information from specific time periods 69 | - Searching by tags 70 | - And many more 71 | 72 | ## Troubleshooting 73 | 74 | If you encounter issues: 75 | 76 | 1. Verify the memory service is running properly as a standalone application 77 | 2. Check that the paths in your `claude mcp add` command are correct 78 | 3. Ensure you have the necessary permissions to execute the specified commands 79 | 4. Try running `claude mcp list` to verify the server was added correctly 80 | 81 | ## Additional Information 82 | 83 | For more detailed information about the memory service's capabilities and configuration options, refer to the main README and other documentation sections. 84 | 85 | ## Benefits of Using Claude Code with MCP Memory Service 86 | 87 | Integrating the MCP Memory Service with Claude Code provides several advantages: 88 | 89 | 1. **Persistent Memory**: Your conversations and stored information persist across sessions 90 | 2. **Semantic Search**: Claude can retrieve relevant information even when not phrased exactly the same way 91 | 3. **Temporal Recall**: Ask about information from specific time periods (e.g., "last week", "yesterday") 92 | 4. **Organized Knowledge**: Use tags to categorize and later retrieve information by category 93 | 94 | This integration helps create a more powerful and versatile CLI experience with Claude, turning it into a knowledge management system with long-term memory capabilities. 95 | -------------------------------------------------------------------------------- /docs/guides/docker.md: -------------------------------------------------------------------------------- 1 | # Docker Installation Guide 2 | 3 | This guide provides detailed instructions for running the MCP Memory Service using Docker. 4 | 5 | ## Prerequisites 6 | 7 | - Docker and Docker Compose installed on your system 8 | - Basic knowledge of Docker concepts 9 | - Sufficient disk space for Docker images and container volumes 10 | 11 | ## Quick Start 12 | 13 | The simplest way to run the Memory Service is using Docker Compose: 14 | 15 | ```bash 16 | # Clone the repository 17 | git clone https://github.com/doobidoo/mcp-memory-service.git 18 | cd mcp-memory-service 19 | 20 | # Start the service 21 | docker-compose up 22 | ``` 23 | 24 | This will: 25 | - Build a Docker image for the Memory Service 26 | - Create persistent volumes for the database and backups 27 | - Start the service on port 8000 28 | 29 | ## Docker Compose Options 30 | 31 | We provide multiple Docker Compose configurations to accommodate different environments and preferences: 32 | 33 | ### Standard Configuration (recommended) 34 | 35 | **File**: `docker-compose.yml` 36 | 37 | This configuration installs the package in development mode, ensuring the module is properly accessible: 38 | 39 | ```bash 40 | docker-compose up 41 | ``` 42 | 43 | ### UV Package Manager Configuration 44 | 45 | **File**: `docker-compose.uv.yml` 46 | 47 | This configuration uses the UV package manager and the wrapper script, which matches the Dockerfile approach: 48 | 49 | ```bash 50 | docker-compose -f docker-compose.uv.yml up 51 | ``` 52 | 53 | ### PYTHONPATH Configuration 54 | 55 | **File**: `docker-compose.pythonpath.yml` 56 | 57 | This configuration explicitly sets the PYTHONPATH to include the source directory: 58 | 59 | ```bash 60 | docker-compose -f docker-compose.pythonpath.yml up 61 | ``` 62 | 63 | ## Environment Variables 64 | 65 | You can customize the service by setting environment variables in your Docker Compose file: 66 | 67 | ```yaml 68 | environment: 69 | - MCP_MEMORY_CHROMA_PATH=/app/chroma_db 70 | - MCP_MEMORY_BACKUPS_PATH=/app/backups 71 | - LOG_LEVEL=INFO 72 | - MAX_RESULTS_PER_QUERY=10 73 | - SIMILARITY_THRESHOLD=0.7 74 | ``` 75 | 76 | ## Volume Management 77 | 78 | The service uses two persistent volumes: 79 | 80 | 1. **ChromaDB Volume**: Stores the vector database 81 | - Default path: `/app/chroma_db` 82 | - Maps to: `${CHROMA_DB_PATH:-$HOME/mcp-memory/chroma_db}` 83 | 84 | 2. **Backups Volume**: Stores database backups 85 | - Default path: `/app/backups` 86 | - Maps to: `${BACKUPS_PATH:-$HOME/mcp-memory/backups}` 87 | 88 | You can customize these paths by setting environment variables before running Docker Compose: 89 | 90 | ```bash 91 | export CHROMA_DB_PATH=/custom/path/to/chroma_db 92 | export BACKUPS_PATH=/custom/path/to/backups 93 | docker-compose up 94 | ``` 95 | 96 | ## Building Custom Images 97 | 98 | To build and run a custom Docker image manually: 99 | 100 | ```bash 101 | # Build the image 102 | docker build -t mcp-memory-service . 103 | 104 | # Run the container 105 | docker run -p 8000:8000 \ 106 | -v /path/to/chroma_db:/app/chroma_db \ 107 | -v /path/to/backups:/app/backups \ 108 | -e LOG_LEVEL=INFO \ 109 | mcp-memory-service 110 | ``` 111 | 112 | ## Troubleshooting 113 | 114 | ### Module Not Found Error 115 | 116 | If you see a `ModuleNotFoundError` for `mcp_memory_service`, it means the Python module is not in the Python path. Try: 117 | 118 | 1. Using the standard `docker-compose.yml` file which installs the package with `pip install -e .` 119 | 2. Using the `docker-compose.uv.yml` file which uses the wrapper script 120 | 3. Using the `docker-compose.pythonpath.yml` file which explicitly sets the PYTHONPATH 121 | 122 | ### Port Conflicts 123 | 124 | If port 8000 is already in use, modify the port mapping in the Docker Compose file: 125 | 126 | ```yaml 127 | ports: 128 | - "8001:8000" # Map container port 8000 to host port 8001 129 | ``` 130 | 131 | ### Permission Issues 132 | 133 | If you encounter permission issues with the volumes, ensure the host directories exist and have appropriate permissions: 134 | 135 | ```bash 136 | mkdir -p ~/mcp-memory/chroma_db ~/mcp-memory/backups 137 | chmod 777 ~/mcp-memory/chroma_db ~/mcp-memory/backups 138 | ``` 139 | 140 | ## Performance Considerations 141 | 142 | The Memory Service can be resource-intensive, especially when using larger embedding models. Consider these settings for optimal Docker performance: 143 | 144 | 1. Allocate sufficient memory to Docker (at least 2GB, 4GB+ recommended) 145 | 2. Use volume mounts on fast storage for better database performance 146 | 3. Set appropriate environment variables for your hardware: 147 | ```yaml 148 | environment: 149 | - MCP_MEMORY_BATCH_SIZE=4 # Smaller batch size for limited resources 150 | - MCP_MEMORY_FORCE_CPU=1 # Force CPU mode if GPU acceleration isn't needed 151 | ``` 152 | 153 | ## Security Notes 154 | 155 | - The Docker configuration exposes port 8000, which should not be accessible from the internet 156 | - No authentication is provided by default; use appropriate network isolation 157 | - The service is designed for local development and operation, not for production deployment 158 | 159 | ## Next Steps 160 | 161 | After setting up the Docker container, you can: 162 | 163 | 1. Configure Claude to use the Memory Service (see [Claude Configuration Guide](claude_config.md)) 164 | 2. Test the service by storing and retrieving memories 165 | 3. Customize the service settings for your specific needs -------------------------------------------------------------------------------- /docs/guides/migration.md: -------------------------------------------------------------------------------- 1 | # Memory Migration Guide 2 | 3 | This guide provides step-by-step instructions for migrating memories between different ChromaDB instances using the MCP Memory Service. 4 | 5 | ## Prerequisites 6 | 7 | Before starting the migration process, ensure you have: 8 | 9 | 1. Python 3.10 or later installed 10 | 2. Required packages installed (check `requirements.txt`) 11 | 3. Access to both source and target ChromaDB instances 12 | 4. Sufficient disk space for local migrations 13 | 5. Network access for remote migrations 14 | 15 | ## Step 1: Environment Verification 16 | 17 | First, verify your environment is properly configured: 18 | 19 | ```bash 20 | python scripts/verify_environment.py 21 | ``` 22 | 23 | This will check: 24 | - Python version compatibility 25 | - Required package installations 26 | - ChromaDB paths and configurations 27 | - Network connectivity (for remote migrations) 28 | 29 | ## Step 2: Choose Migration Type 30 | 31 | ### Option A: Local to Remote Migration 32 | Use this option to move memories from your local development environment to a remote production server. 33 | 34 | ### Option B: Remote to Local Migration 35 | Use this option to create local backups or set up a development environment with existing memories. 36 | 37 | ## Step 3: Prepare Configuration 38 | 39 | ### For Local to Remote Migration 40 | 1. Identify your local ChromaDB path 41 | 2. Note the remote server's host and port 42 | 3. Prepare the configuration: 43 | ```json 44 | { 45 | "source_type": "local", 46 | "source_config": "/path/to/local/chroma", 47 | "target_type": "remote", 48 | "target_config": { 49 | "host": "remote-host", 50 | "port": 8000 51 | } 52 | } 53 | ``` 54 | 55 | ### For Remote to Local Migration 56 | 1. Note the remote server's host and port 57 | 2. Choose a local path for the ChromaDB 58 | 3. Prepare the configuration: 59 | ```json 60 | { 61 | "source_type": "remote", 62 | "source_config": { 63 | "host": "remote-host", 64 | "port": 8000 65 | }, 66 | "target_type": "local", 67 | "target_config": "/path/to/local/chroma" 68 | } 69 | ``` 70 | 71 | ## Step 4: Run Migration 72 | 73 | ### Using Command Line 74 | ```bash 75 | # Local to Remote Migration 76 | python scripts/mcp-migration.py \ 77 | --source-type local \ 78 | --source-config /path/to/local/chroma \ 79 | --target-type remote \ 80 | --target-config '{"host": "remote-host", "port": 8000}' 81 | 82 | # Remote to Local Migration 83 | python scripts/mcp-migration.py \ 84 | --source-type remote \ 85 | --source-config '{"host": "remote-host", "port": 8000}' \ 86 | --target-type local \ 87 | --target-config /path/to/local/chroma 88 | ``` 89 | 90 | ### Using Python Script 91 | ```python 92 | from scripts.mcp_migration import migrate_memories 93 | 94 | # Local to Remote Migration 95 | migrate_memories( 96 | source_type='local', 97 | source_config='/path/to/local/chroma', 98 | target_type='remote', 99 | target_config={'host': 'remote-host', 'port': 8000} 100 | ) 101 | 102 | # Remote to Local Migration 103 | migrate_memories( 104 | source_type='remote', 105 | source_config={'host': 'remote-host', 'port': 8000}, 106 | target_type='local', 107 | target_config='/path/to/local/chroma' 108 | ) 109 | ``` 110 | 111 | ## Step 5: Monitor Progress 112 | 113 | The migration script provides detailed logging: 114 | - Connection status 115 | - Collection verification 116 | - Batch processing progress 117 | - Error messages (if any) 118 | 119 | Monitor the output for: 120 | - Successful connection messages 121 | - Batch processing updates 122 | - Any error messages 123 | - Final verification results 124 | 125 | ## Step 6: Verify Migration 126 | 127 | After migration completes: 128 | 1. Check the target collection for expected number of memories 129 | 2. Verify a sample of memories for content integrity 130 | 3. Test memory access through the MCP Memory Service 131 | 132 | ## Troubleshooting 133 | 134 | ### Common Issues 135 | 136 | 1. **Connection Failures** 137 | - Verify network connectivity 138 | - Check firewall settings 139 | - Validate host and port configurations 140 | 141 | 2. **Permission Issues** 142 | - Check file permissions for local paths 143 | - Verify user access rights 144 | - Ensure proper directory ownership 145 | 146 | 3. **Data Transfer Errors** 147 | - Check disk space 148 | - Verify collection permissions 149 | - Monitor system resources 150 | 151 | ### Error Messages 152 | 153 | 1. **"Failed to connect to ChromaDB"** 154 | - Verify ChromaDB is running 155 | - Check network connectivity 156 | - Validate configuration 157 | 158 | 2. **"Collection not found"** 159 | - Verify collection name 160 | - Check collection permissions 161 | - Ensure collection exists 162 | 163 | 3. **"Insufficient disk space"** 164 | - Free up disk space 165 | - Choose a different target location 166 | - Consider cleaning up old data 167 | 168 | ## Best Practices 169 | 170 | 1. **Before Migration** 171 | - Backup existing data 172 | - Verify environment compatibility 173 | - Check system resources 174 | 175 | 2. **During Migration** 176 | - Monitor progress 177 | - Avoid interrupting the process 178 | - Check for error messages 179 | 180 | 3. **After Migration** 181 | - Verify data integrity 182 | - Test memory access 183 | - Document the migration 184 | 185 | ## Additional Resources 186 | 187 | - [Technical Documentation](technical/memory-migration.md) - Detailed technical information 188 | - [Troubleshooting Guide](troubleshooting.md) - Common issues and solutions 189 | - [Configuration Guide](../README.md#configuration-options) - Available settings and options -------------------------------------------------------------------------------- /docs/guides/scripts.md: -------------------------------------------------------------------------------- 1 | # Scripts Documentation 2 | 3 | This document provides an overview of the available scripts in the `scripts/` directory and their purposes. 4 | 5 | ## Essential Scripts 6 | 7 | ### Server Management 8 | - `run_memory_server.py`: Main script to start the memory service server 9 | ```bash 10 | python scripts/run_memory_server.py 11 | ``` 12 | 13 | ### Environment Verification 14 | - `verify_environment.py`: Verifies the installation environment and dependencies 15 | ```bash 16 | python scripts/verify_environment.py 17 | ``` 18 | 19 | ### Installation Testing 20 | - `test_installation.py`: Tests the installation and basic functionality 21 | ```bash 22 | python scripts/test_installation.py 23 | ``` 24 | 25 | ### Memory Management 26 | - `validate_memories.py`: Validates the integrity of stored memories 27 | ```bash 28 | python scripts/validate_memories.py 29 | ``` 30 | - `repair_memories.py`: Repairs corrupted or invalid memories 31 | ```bash 32 | python scripts/repair_memories.py 33 | ``` 34 | - `list-collections.py`: Lists all available memory collections 35 | ```bash 36 | python scripts/list-collections.py 37 | ``` 38 | 39 | ## Migration Scripts 40 | - `mcp-migration.py`: Handles migration of MCP-related data 41 | ```bash 42 | python scripts/mcp-migration.py 43 | ``` 44 | - `memory-migration.py`: Handles migration of memory data 45 | ```bash 46 | python scripts/memory-migration.py 47 | ``` 48 | 49 | ## Troubleshooting Scripts 50 | - `verify_pytorch_windows.py`: Verifies PyTorch installation on Windows 51 | ```bash 52 | python scripts/verify_pytorch_windows.py 53 | ``` 54 | - `verify_torch.py`: General PyTorch verification 55 | ```bash 56 | python scripts/verify_torch.py 57 | ``` 58 | 59 | ## Usage Notes 60 | - Most scripts can be run directly with Python 61 | - Some scripts may require specific environment variables to be set 62 | - Always run verification scripts after installation or major updates 63 | - Use migration scripts with caution and ensure backups are available 64 | 65 | ## Script Dependencies 66 | - Python 3.10+ 67 | - Required packages listed in `requirements.txt` 68 | - Some scripts may require additional dependencies listed in `requirements-migration.txt` -------------------------------------------------------------------------------- /docs/guides/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # MCP Memory Service Troubleshooting Guide 2 | 3 | This guide covers common issues and their solutions when working with the MCP Memory Service. 4 | 5 | ## Common Installation Issues 6 | 7 | [Content from installation.md's troubleshooting section - already well documented] 8 | 9 | ## MCP Protocol Issues 10 | 11 | ### Method Not Found Errors 12 | 13 | If you're seeing "Method not found" errors or JSON error popups in Claude Desktop: 14 | 15 | #### Symptoms 16 | - "Method not found" errors in logs 17 | - JSON error popups in Claude Desktop 18 | - Connection issues between Claude Desktop and the memory service 19 | 20 | #### Solution 21 | 1. Ensure you have the latest version of the MCP Memory Service 22 | 2. Verify your server implements all required MCP protocol methods: 23 | - resources/list 24 | - resources/read 25 | - resource_templates/list 26 | 3. Update your Claude Desktop configuration using the provided template 27 | 28 | [Additional content from MCP_PROTOCOL_FIX.md] 29 | 30 | ## Windows-Specific Issues 31 | 32 | [Content from WINDOWS_JSON_FIX.md and windows-specific sections] 33 | 34 | ## Performance Optimization 35 | 36 | ### Memory Issues 37 | [Content from installation.md's performance section] 38 | 39 | ### Acceleration Issues 40 | [Content from installation.md's acceleration section] 41 | 42 | ## Debugging Tools 43 | 44 | [Content from installation.md's debugging section] 45 | 46 | ## Getting Help 47 | 48 | [Content from installation.md's help section] 49 | -------------------------------------------------------------------------------- /docs/guides/windows-setup.md: -------------------------------------------------------------------------------- 1 | # Windows Setup Guide for MCP Memory Service 2 | 3 | This guide provides comprehensive instructions for setting up and running the MCP Memory Service on Windows systems, including handling common Windows-specific issues. 4 | 5 | ## Installation 6 | 7 | ### Prerequisites 8 | - Python 3.10 or newer 9 | - Git for Windows 10 | - Visual Studio Build Tools (for PyTorch) 11 | 12 | ### Recommended Installation (Using UV) 13 | 14 | 1. Install UV: 15 | ```bash 16 | pip install uv 17 | ``` 18 | 19 | 2. Clone and setup: 20 | ```bash 21 | git clone https://github.com/doobidoo/mcp-memory-service.git 22 | cd mcp-memory-service 23 | uv venv 24 | .venv\Scripts\activate 25 | uv pip install -r requirements.txt 26 | uv pip install -e . 27 | ``` 28 | 29 | ### Alternative: Windows-Specific Installation 30 | 31 | If you encounter issues with UV, use our Windows-specific installation script: 32 | 33 | ```bash 34 | python scripts/install_windows.py 35 | ``` 36 | 37 | This script handles: 38 | 1. Detecting CUDA availability 39 | 2. Installing the correct PyTorch version 40 | 3. Setting up dependencies without conflicts 41 | 4. Verifying the installation 42 | 43 | ## Configuration 44 | 45 | ### Claude Desktop Configuration 46 | 47 | 1. Create or edit your Claude Desktop configuration file: 48 | - Location: `%APPDATA%\Claude\claude_desktop_config.json` 49 | 50 | 2. Add the following configuration: 51 | ```json 52 | { 53 | "memory": { 54 | "command": "python", 55 | "args": [ 56 | "C:\\path\\to\\mcp-memory-service\\memory_wrapper.py" 57 | ], 58 | "env": { 59 | "MCP_MEMORY_CHROMA_PATH": "C:\\Users\\YourUsername\\AppData\\Local\\mcp-memory\\chroma_db", 60 | "MCP_MEMORY_BACKUPS_PATH": "C:\\Users\\YourUsername\\AppData\\Local\\mcp-memory\\backups" 61 | } 62 | } 63 | } 64 | ``` 65 | 66 | ### Environment Variables 67 | 68 | Important Windows-specific environment variables: 69 | ``` 70 | MCP_MEMORY_USE_DIRECTML=1 # Enable DirectML acceleration if CUDA is not available 71 | PYTORCH_ENABLE_MPS_FALLBACK=0 # Disable MPS (not needed on Windows) 72 | ``` 73 | 74 | ## Common Windows-Specific Issues 75 | 76 | ### PyTorch Installation Issues 77 | 78 | If you see errors about PyTorch installation: 79 | 80 | 1. Use the Windows-specific installation script: 81 | ```bash 82 | python scripts/install_windows.py 83 | ``` 84 | 85 | 2. Or manually install PyTorch with the correct index URL: 86 | ```bash 87 | pip install torch==2.1.0 torchvision==2.1.0 torchaudio==2.1.0 --index-url https://download.pytorch.org/whl/cu118 88 | ``` 89 | 90 | ### JSON Parsing Errors 91 | 92 | If you see "Unexpected token" errors in Claude Desktop: 93 | 94 | **Symptoms:** 95 | ``` 96 | Unexpected token 'U', "Using Chro"... is not valid JSON 97 | Unexpected token 'I', "[INFO] Star"... is not valid JSON 98 | ``` 99 | 100 | **Solution:** 101 | - Update to the latest version which includes Windows-specific stream handling fixes 102 | - Use the memory wrapper script which properly handles stdout/stderr separation 103 | 104 | ### Recursion Errors 105 | 106 | If you encounter recursion errors: 107 | 108 | 1. Run the sitecustomize fix script: 109 | ```bash 110 | python scripts/fix_sitecustomize.py 111 | ``` 112 | 113 | 2. Restart your Python environment 114 | 115 | ## Debugging Tools 116 | 117 | Windows-specific debugging tools: 118 | 119 | ```bash 120 | # Verify PyTorch installation 121 | python scripts/verify_pytorch_windows.py 122 | 123 | # Check environment compatibility 124 | python scripts/verify_environment_enhanced.py 125 | 126 | # Test the memory server 127 | python scripts/run_memory_server.py 128 | ``` 129 | 130 | ## Log Files 131 | 132 | Important log locations on Windows: 133 | - Claude Desktop logs: `%APPDATA%\Claude\logs\mcp-server-memory.log` 134 | - Memory service logs: `%LOCALAPPDATA%\mcp-memory\logs\memory_service.log` 135 | 136 | ## Performance Optimization 137 | 138 | ### GPU Acceleration 139 | 140 | 1. CUDA (recommended if available): 141 | - Ensure NVIDIA drivers are up to date 142 | - CUDA toolkit is not required (bundled with PyTorch) 143 | 144 | 2. DirectML (alternative): 145 | - Enable with `MCP_MEMORY_USE_DIRECTML=1` 146 | - Useful for AMD GPUs or when CUDA is not available 147 | 148 | ### Memory Usage 149 | 150 | If experiencing memory issues: 151 | 1. Reduce batch size: 152 | ```bash 153 | set MCP_MEMORY_BATCH_SIZE=4 154 | ``` 155 | 156 | 2. Use a smaller model: 157 | ```bash 158 | set MCP_MEMORY_MODEL_NAME=paraphrase-MiniLM-L3-v2 159 | ``` 160 | 161 | ## Getting Help 162 | 163 | If you encounter Windows-specific issues: 164 | 1. Check the logs in `%APPDATA%\Claude\logs\` 165 | 2. Run verification tools mentioned above 166 | 3. Contact support via Telegram: t.me/doobeedoo -------------------------------------------------------------------------------- /docs/integrations.md: -------------------------------------------------------------------------------- 1 | # MCP Memory Service Integrations 2 | 3 | This document catalogs tools, utilities, and integrations that extend the functionality of the MCP Memory Service. 4 | 5 | ## Official Integrations 6 | 7 | ### [MCP Memory Dashboard](https://github.com/doobidoo/mcp-memory-dashboard)(This is still wip!) 8 | 9 | A web-based dashboard for viewing, searching, and managing your MCP Memory Service data. The dashboard allows you to: 10 | - Browse and search memories 11 | - View memory metadata and tags 12 | - Delete unwanted memories 13 | - Perform semantic searches 14 | - Monitor system health 15 | 16 | ## Community Integrations 17 | 18 | ### [Claude Memory Context](https://github.com/doobidoo/claude-memory-context) 19 | 20 | A utility that enables Claude to start each conversation with awareness of the topics and important memories stored in your MCP Memory Service. 21 | 22 | This tool: 23 | - Queries your MCP memory service for recent and important memories 24 | - Extracts topics and content summaries 25 | - Formats this information into a structured context section 26 | - Updates Claude project instructions automatically 27 | 28 | The utility leverages Claude's project instructions feature without requiring any modifications to the MCP protocol. It can be automated to run periodically, ensuring Claude always has access to your latest memories. 29 | 30 | See the [Claude Memory Context repository](https://github.com/doobidoo/claude-memory-context) for installation and usage instructions. 31 | 32 | --- 33 | 34 | ## Adding Your Integration 35 | 36 | If you've built a tool or integration for the MCP Memory Service, we'd love to include it here. Please submit a pull request that adds your project to this document with: 37 | 38 | 1. The name of your integration (with link to repository) 39 | 2. A brief description (2-3 sentences) 40 | 3. A list of key features 41 | 4. Any installation notes or special requirements 42 | 43 | All listed integrations should be functional, documented, and actively maintained. 44 | -------------------------------------------------------------------------------- /docs/technical/development.md: -------------------------------------------------------------------------------- 1 | # MCP Memory Service - Development Guidelines 2 | 3 | ## Commands 4 | - Run memory server: `python scripts/run_memory_server.py` 5 | - Run tests: `pytest tests/` 6 | - Run specific test: `pytest tests/test_memory_ops.py::test_store_memory -v` 7 | - Check environment: `python scripts/verify_environment_enhanced.py` 8 | - Windows installation: `python scripts/install_windows.py` 9 | - Build package: `python -m build` 10 | 11 | ## Installation Guidelines 12 | - Always install in a virtual environment: `python -m venv venv` 13 | - Use `install.py` for cross-platform installation 14 | - Windows requires special PyTorch installation with correct index URL: 15 | ```bash 16 | pip install torch==2.1.0 torchvision==2.1.0 torchaudio==2.1.0 --index-url https://download.pytorch.org/whl/cu118 17 | ``` 18 | - For recursion errors, run: `python scripts/fix_sitecustomize.py` 19 | 20 | ## Code Style 21 | - Python 3.10+ with type hints 22 | - Use dataclasses for models (see `models/memory.py`) 23 | - Triple-quoted docstrings for modules and functions 24 | - Async/await pattern for all I/O operations 25 | - Error handling with specific exception types and informative messages 26 | - Logging with appropriate levels for different severity 27 | - Commit messages follow semantic release format: `type(scope): message` 28 | 29 | ## Project Structure 30 | - `src/mcp_memory_service/` - Core package code 31 | - `models/` - Data models 32 | - `storage/` - Database abstraction 33 | - `utils/` - Helper functions 34 | - `server.py` - MCP protocol implementation 35 | - `scripts/` - Utility scripts 36 | - `memory_wrapper.py` - Windows wrapper script 37 | - `install.py` - Cross-platform installation script 38 | 39 | ## Dependencies 40 | - ChromaDB (0.5.23) for vector database 41 | - sentence-transformers (>=2.2.2) for embeddings 42 | - PyTorch (platform-specific installation) 43 | - MCP protocol (>=1.0.0, <2.0.0) for client-server communication 44 | 45 | ## Troubleshooting 46 | - For Windows installation issues, use `scripts/install_windows.py` 47 | - Apple Silicon requires Python 3.10+ built for ARM64 48 | - CUDA issues: verify with `torch.cuda.is_available()` 49 | - For MCP protocol issues, check `server.py` for required methods 50 | 51 | ## Debugging with MCP-Inspector 52 | 53 | To debug the MCP-MEMORY-SERVICE using the [MCP-Inspector](https://modelcontextprotocol.io/docs/tools/inspector) tool, you can use the following command pattern: 54 | 55 | ```bash 56 | MCP_MEMORY_CHROMA_PATH="/path/to/your/chroma_db" MCP_MEMORY_BACKUPS_PATH="/path/to/your/backups" npx @modelcontextprotocol/inspector uv --directory /path/to/mcp-memory-service run memory 57 | ``` 58 | 59 | Replace the paths with your specific directories: 60 | - `/path/to/your/chroma_db`: Location where Chroma database files are stored 61 | - `/path/to/your/backups`: Location for memory backups 62 | - `/path/to/mcp-memory-service`: Directory containing the MCP-MEMORY-SERVICE code 63 | 64 | For example: 65 | ```bash 66 | MCP_MEMORY_CHROMA_PATH="~/Library/Mobile Documents/com~apple~CloudDocs/AI/claude-memory/chroma_db" MCP_MEMORY_BACKUPS_PATH="~/Library/Mobile Documents/com~apple~CloudDocs/AI/claude-memory/backups" npx @modelcontextprotocol/inspector uv --directory ~/Documents/GitHub/mcp-memory-service run memory 67 | ``` 68 | 69 | This command sets the required environment variables and runs the memory service through the inspector tool for debugging purposes. 70 | -------------------------------------------------------------------------------- /docs/technical/docker-deployment.md: -------------------------------------------------------------------------------- 1 | # Docker Deployment 2 | 3 | ## Overview 4 | 5 | The MCP Memory Service can be deployed using Docker for platform-agnostic installation and operation. The Docker configuration uses UV for dependency management, ensuring consistent performance across different environments. 6 | 7 | ## Prerequisites 8 | 9 | - Docker installed on your system 10 | - Docker Compose (optional, for simplified deployment) 11 | 12 | ## Quick Start 13 | 14 | ### Using Docker Directly 15 | 16 | ```bash 17 | # Build the Docker image 18 | docker build -t mcp-memory-service . 19 | 20 | # Create directories for persistent storage 21 | mkdir -p ./data/chroma_db ./data/backups 22 | 23 | # Run with default settings 24 | docker run -d -p 8000:8000 --name memory-service \ 25 | -v ./data/chroma_db:/app/chroma_db \ 26 | -v ./data/backups:/app/backups \ 27 | mcp-memory-service 28 | ``` 29 | 30 | ### Using Docker Compose 31 | 32 | ```bash 33 | # Create data directories 34 | mkdir -p ./data/chroma_db ./data/backups 35 | 36 | # Start the service 37 | docker-compose up -d 38 | ``` 39 | 40 | ## Configuration Options 41 | 42 | ### Environment Variables 43 | 44 | The Docker container supports the following environment variables: 45 | 46 | | Variable | Description | Default | 47 | |----------|-------------|--------| 48 | | `MCP_MEMORY_CHROMA_PATH` | Path to ChromaDB storage | `/app/chroma_db` | 49 | | `MCP_MEMORY_BACKUPS_PATH` | Path to backups storage | `/app/backups` | 50 | | `LOG_LEVEL` | Logging level (DEBUG, INFO, WARNING, ERROR) | `INFO` | 51 | | `MAX_RESULTS_PER_QUERY` | Maximum number of results returned per query | `10` | 52 | | `SIMILARITY_THRESHOLD` | Threshold for similarity search | `0.7` | 53 | | `MCP_MEMORY_FORCE_CPU` | Force CPU-only mode | `0` | 54 | | `PYTORCH_ENABLE_MPS_FALLBACK` | Enable MPS fallback for Apple Silicon | `1` | 55 | 56 | ### Example: Custom Configuration 57 | 58 | ```bash 59 | docker run -d -p 8000:8000 --name memory-service \ 60 | -v ./data/chroma_db:/app/chroma_db \ 61 | -v ./data/backups:/app/backups \ 62 | -e MCP_MEMORY_CHROMA_PATH=/app/chroma_db \ 63 | -e MCP_MEMORY_BACKUPS_PATH=/app/backups \ 64 | -e LOG_LEVEL=DEBUG \ 65 | -e MAX_RESULTS_PER_QUERY=20 \ 66 | -e SIMILARITY_THRESHOLD=0.8 \ 67 | mcp-memory-service 68 | ``` 69 | 70 | ## Integration with Claude Desktop 71 | 72 | To use the Docker container with Claude Desktop, update your `claude_desktop_config.json` file: 73 | 74 | ```json 75 | { 76 | "memory": { 77 | "command": "docker", 78 | "args": [ 79 | "run", 80 | "--rm", 81 | "-p", "8000:8000", 82 | "-v", "/path/to/data/chroma_db:/app/chroma_db", 83 | "-v", "/path/to/data/backups:/app/backups", 84 | "-e", "MCP_MEMORY_CHROMA_PATH=/app/chroma_db", 85 | "-e", "MCP_MEMORY_BACKUPS_PATH=/app/backups", 86 | "mcp-memory-service" 87 | ] 88 | } 89 | } 90 | ``` 91 | 92 | ## Data Persistence 93 | 94 | The Docker configuration uses volumes to persist data: 95 | 96 | - `/app/chroma_db`: ChromaDB database files 97 | - `/app/backups`: Backup files 98 | 99 | Mount these volumes to local directories for data persistence: 100 | 101 | ```bash 102 | docker run -d -p 8000:8000 --name memory-service \ 103 | -v /path/on/host/chroma_db:/app/chroma_db \ 104 | -v /path/on/host/backups:/app/backups \ 105 | mcp-memory-service 106 | ``` 107 | 108 | ## Performance Considerations 109 | 110 | ### Resource Allocation 111 | 112 | You can limit container resources using Docker's resource constraints: 113 | 114 | ```bash 115 | docker run -d -p 8000:8000 --name memory-service \ 116 | --memory=2g \ 117 | --cpus=2 \ 118 | -v ./data/chroma_db:/app/chroma_db \ 119 | -v ./data/backups:/app/backups \ 120 | mcp-memory-service 121 | ``` 122 | 123 | ### GPU Access 124 | 125 | To use GPU acceleration (for NVIDIA GPUs): 126 | 127 | ```bash 128 | docker run -d -p 8000:8000 --name memory-service \ 129 | --gpus all \ 130 | -v ./data/chroma_db:/app/chroma_db \ 131 | -v ./data/backups:/app/backups \ 132 | mcp-memory-service 133 | ``` 134 | 135 | ## Troubleshooting 136 | 137 | ### View Container Logs 138 | 139 | ```bash 140 | docker logs memory-service 141 | ``` 142 | 143 | ### Interactive Debugging 144 | 145 | ```bash 146 | docker exec -it memory-service bash 147 | ``` 148 | 149 | ### Common Issues 150 | 151 | 1. **Permission Errors** 152 | 153 | If you see permission errors when accessing the volumes: 154 | 155 | ```bash 156 | # On host machine 157 | chmod -R 777 ./data/chroma_db ./data/backups 158 | ``` 159 | 160 | 2. **Connection Refused** 161 | 162 | If Claude Desktop can't connect to the memory service: 163 | 164 | - Verify the container is running: `docker ps` 165 | - Check exposed ports: `docker port memory-service` 166 | - Test locally: `curl http://localhost:8000/health` 167 | 168 | ## Building Custom Images 169 | 170 | You can build custom images with additional dependencies: 171 | 172 | ```dockerfile 173 | FROM mcp-memory-service:latest 174 | 175 | # Install additional packages 176 | RUN python -m uv pip install pandas scikit-learn 177 | 178 | # Copy custom configuration 179 | COPY my_config.json /app/config.json 180 | ``` 181 | 182 | Then build and run the custom image: 183 | 184 | ```bash 185 | docker build -t custom-memory-service -f Dockerfile.custom . 186 | docker run -d -p 8000:8000 custom-memory-service 187 | ``` 188 | -------------------------------------------------------------------------------- /docs/technical/memory-migration.md: -------------------------------------------------------------------------------- 1 | # Memory Migration Technical Documentation 2 | 3 | This document provides technical details about the memory migration process in the MCP Memory Service. 4 | 5 | ## Overview 6 | 7 | The memory migration process allows transferring memories between different ChromaDB instances, supporting both local and remote migrations. The process is handled by the `mcp-migration.py` script, which provides a robust and efficient way to move memories while maintaining data integrity. 8 | 9 | ## Migration Types 10 | 11 | ### 1. Local to Remote Migration 12 | - Source: Local ChromaDB instance 13 | - Target: Remote ChromaDB server 14 | - Use case: Moving memories from a development environment to production 15 | 16 | ### 2. Remote to Local Migration 17 | - Source: Remote ChromaDB server 18 | - Target: Local ChromaDB instance 19 | - Use case: Creating local backups or development environments 20 | 21 | ## Technical Implementation 22 | 23 | ### Environment Verification 24 | Before starting the migration, the script performs environment verification: 25 | - Checks Python version compatibility 26 | - Verifies required packages are installed 27 | - Validates ChromaDB paths and configurations 28 | - Ensures network connectivity for remote migrations 29 | 30 | ### Migration Process 31 | 1. **Connection Setup** 32 | - Establishes connections to both source and target ChromaDB instances 33 | - Verifies collection existence and creates if necessary 34 | - Sets up embedding functions for consistent vectorization 35 | 36 | 2. **Data Transfer** 37 | - Implements batch processing (default batch size: 10) 38 | - Includes delay between batches to prevent overwhelming the target 39 | - Handles duplicate detection to avoid data redundancy 40 | - Maintains metadata and document relationships 41 | 42 | 3. **Verification** 43 | - Validates successful transfer by comparing record counts 44 | - Checks for data integrity 45 | - Provides detailed logging of the migration process 46 | 47 | ## Error Handling 48 | 49 | The migration script includes comprehensive error handling for: 50 | - Connection failures 51 | - Collection access issues 52 | - Data transfer interruptions 53 | - Configuration errors 54 | - Environment incompatibilities 55 | 56 | ## Performance Considerations 57 | 58 | - **Batch Size**: Default 10 records per batch 59 | - **Delay**: 1 second between batches 60 | - **Memory Usage**: Optimized for minimal memory footprint 61 | - **Network**: Handles connection timeouts and retries 62 | 63 | ## Configuration Options 64 | 65 | ### Source Configuration 66 | ```json 67 | { 68 | "type": "local|remote", 69 | "config": { 70 | "path": "/path/to/chroma", // for local 71 | "host": "remote-host", // for remote 72 | "port": 8000 // for remote 73 | } 74 | } 75 | ``` 76 | 77 | ### Target Configuration 78 | ```json 79 | { 80 | "type": "local|remote", 81 | "config": { 82 | "path": "/path/to/chroma", // for local 83 | "host": "remote-host", // for remote 84 | "port": 8000 // for remote 85 | } 86 | } 87 | ``` 88 | 89 | ## Best Practices 90 | 91 | 1. **Pre-Migration** 92 | - Verify environment compatibility 93 | - Ensure sufficient disk space 94 | - Check network connectivity for remote migrations 95 | - Backup existing data 96 | 97 | 2. **During Migration** 98 | - Monitor progress through logs 99 | - Avoid interrupting the process 100 | - Check for error messages 101 | 102 | 3. **Post-Migration** 103 | - Verify data integrity 104 | - Check collection statistics 105 | - Validate memory access 106 | 107 | ## Troubleshooting 108 | 109 | Common issues and solutions: 110 | 111 | 1. **Connection Failures** 112 | - Verify network connectivity 113 | - Check firewall settings 114 | - Validate host and port configurations 115 | 116 | 2. **Data Transfer Issues** 117 | - Check disk space 118 | - Verify collection permissions 119 | - Monitor system resources 120 | 121 | 3. **Environment Issues** 122 | - Run environment verification 123 | - Check package versions 124 | - Validate Python environment 125 | 126 | ## Example Usage 127 | 128 | ### Command Line 129 | ```bash 130 | # Local to Remote Migration 131 | python scripts/mcp-migration.py \ 132 | --source-type local \ 133 | --source-config /path/to/local/chroma \ 134 | --target-type remote \ 135 | --target-config '{"host": "remote-host", "port": 8000}' 136 | 137 | # Remote to Local Migration 138 | python scripts/mcp-migration.py \ 139 | --source-type remote \ 140 | --source-config '{"host": "remote-host", "port": 8000}' \ 141 | --target-type local \ 142 | --target-config /path/to/local/chroma 143 | ``` 144 | 145 | ### Programmatic Usage 146 | ```python 147 | from scripts.mcp_migration import migrate_memories 148 | 149 | # Local to Remote Migration 150 | migrate_memories( 151 | source_type='local', 152 | source_config='/path/to/local/chroma', 153 | target_type='remote', 154 | target_config={'host': 'remote-host', 'port': 8000} 155 | ) 156 | 157 | # Remote to Local Migration 158 | migrate_memories( 159 | source_type='remote', 160 | source_config={'host': 'remote-host', 'port': 8000}, 161 | target_type='local', 162 | target_config='/path/to/local/chroma' 163 | ) 164 | ``` -------------------------------------------------------------------------------- /docs/technical/tag-storage.md: -------------------------------------------------------------------------------- 1 | # Tag Storage Procedure 2 | 3 | ## File Structure Overview 4 | ``` 5 | mcp_memory_service/ 6 | ├── tests/ 7 | │ └── test_tag_storage.py # Integration tests 8 | ├── scripts/ 9 | │ ├── validate_memories.py # Validation script 10 | │ └── migrate_tags.py # Migration script 11 | ``` 12 | 13 | ## Execution Steps 14 | 15 | 1. **Run Initial Validation** 16 | ```bash 17 | python scripts/validate_memories.py 18 | ``` 19 | - Generates validation report of current state 20 | 21 | 2. **Run Integration Tests** 22 | ```bash 23 | python tests/test_tag_storage.py 24 | ``` 25 | - Verifies functionality 26 | 27 | 3. **Execute Migration** 28 | ```bash 29 | python scripts/migrate_tags.py 30 | ``` 31 | The script will: 32 | - Create a backup automatically 33 | - Run validation check 34 | - Ask for confirmation before proceeding 35 | - Perform migration 36 | - Verify the migration 37 | 38 | 4. **Post-Migration Validation** 39 | ```bash 40 | python scripts/validate_memories.py 41 | ``` 42 | - Confirms successful migration 43 | 44 | ## Monitoring Requirements 45 | - Keep backup files for at least 7 days 46 | - Monitor logs for any tag-related errors 47 | - Run validation script daily for the first week 48 | - Check search functionality with various tag formats 49 | 50 | ## Rollback Process 51 | If issues are detected, use: 52 | ```bash 53 | python scripts/migrate_tags.py --rollback 54 | ``` -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Heinrich Krupp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /mcpServers.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "memory": { 4 | "command": "docker", 5 | "args": [ 6 | "run", 7 | "-i", 8 | "--rm", 9 | "-v", "$HOME/mcp-memory/chroma_db:/app/chroma_db", 10 | "-v", "$HOME/mcp-memory/backups:/app/backups", 11 | "mcp-memory-service:latest" 12 | ], 13 | "env": { 14 | "MCP_MEMORY_CHROMA_PATH": "/app/chroma_db", 15 | "MCP_MEMORY_BACKUPS_PATH": "/app/backups" 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /mcp_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": { 3 | "memory": { 4 | "command": "uv", 5 | "args": ["run", "-m", "mcp_memory_service.server"], 6 | "cwd": "/Users/hkr/Documents/GitHub/mcp-memory-service" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "python-semantic-release"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "mcp-memory-service" 7 | version = "0.2.1" 8 | description = "A semantic memory service using ChromaDB and sentence-transformers" 9 | readme = "README.md" 10 | requires-python = ">=3.10" 11 | authors = [ 12 | { name = "Heinrich Krupp", email = "heinrich.krupp@gmail.com" } 13 | ] 14 | license = { text = "MIT" } 15 | dependencies = [ 16 | "chromadb==0.5.23", 17 | # Looser constraint for sentence-transformers to allow for platform-specific versions 18 | # Actual version will be managed by install.py based on platform 19 | "sentence-transformers", 20 | "tokenizers==0.20.3", 21 | "mcp>=1.0.0,<2.0.0" 22 | ] 23 | 24 | [project.scripts] 25 | memory = "mcp_memory_service.server:main" 26 | 27 | [tool.hatch.build.targets.wheel] 28 | packages = ["src/mcp_memory_service"] 29 | 30 | [tool.hatch.version] 31 | path = "src/mcp_memory_service/__init__.py" 32 | 33 | [tool.semantic_release] 34 | version_variable = [ 35 | "src/mcp_memory_service/__init__.py:__version__", 36 | "pyproject.toml:version" 37 | ] 38 | branch = "main" 39 | changelog_file = "CHANGELOG.md" 40 | build_command = "python -m build" 41 | dist_path = "dist/" 42 | upload_to_pypi = true 43 | upload_to_release = true 44 | commit_message = "chore(release): bump version to {version}" 45 | 46 | [tool.semantic_release.commit_parser_options] 47 | allowed_tags = [ 48 | "build", 49 | "chore", 50 | "ci", 51 | "docs", 52 | "feat", 53 | "fix", 54 | "perf", 55 | "style", 56 | "refactor", 57 | "test" 58 | ] 59 | minor_tags = ["feat"] 60 | patch_tags = ["fix", "perf"] 61 | 62 | [tool.semantic_release.changelog] 63 | template_dir = "templates" 64 | changelog_sections = [ 65 | ["feat", "Features"], 66 | ["fix", "Bug Fixes"], 67 | ["perf", "Performance"], 68 | ["refactor", "Code Refactoring"], 69 | ["test", "Tests"] 70 | ] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Core dependencies 2 | chromadb==0.5.23 3 | tokenizers==0.20.3 4 | websockets>=11.0.3 5 | mcp>=1.0.0,<2.0.0 6 | 7 | # Platform-specific recommendations (DO NOT UNCOMMENT - handled by install.py) 8 | 9 | # For macOS with Intel CPUs: 10 | # torch==2.0.1 11 | # torchvision==2.0.1 12 | # torchaudio==2.0.1 13 | # sentence-transformers==2.2.2 14 | 15 | # For macOS with Apple Silicon: 16 | # torch>=2.0.0 17 | # torchvision>=0.15.0 18 | # torchaudio>=2.0.0 19 | # sentence-transformers>=2.2.2 20 | 21 | # For Windows with NVIDIA GPU: 22 | # pip install torch==2.1.0 torchvision==2.1.0 torchaudio==2.1.0 --index-url=https://download.pytorch.org/whl/cu118 23 | # sentence-transformers>=2.2.2 24 | 25 | # For Windows with DirectML: 26 | # torch==2.1.0 27 | # torchvision==2.1.0 28 | # torchaudio==2.1.0 29 | # torch-directml>=0.2.0 30 | # sentence-transformers>=2.2.2 31 | 32 | # For Linux with CUDA: 33 | # torch>=2.0.0 34 | # torchvision>=0.15.0 35 | # torchaudio>=2.0.0 36 | # sentence-transformers>=2.2.2 37 | 38 | # For CPU-only fallback (all platforms): 39 | # torch==1.13.1 40 | # torchvision==0.14.1 41 | # torchaudio==0.13.1 42 | # sentence-transformers==2.2.2 43 | # onnxruntime>=1.15.0 44 | 45 | # Note: PyTorch and sentence-transformers will be installed by install.py with 46 | # appropriate platform-specific versions. Do not install manually unless you 47 | # encounter issues with the automatic installation. -------------------------------------------------------------------------------- /requirements_current.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.8.0 3 | asgiref==3.8.1 4 | backoff==2.2.1 5 | bcrypt==4.2.1 6 | build==1.2.2.post1 7 | cachetools==5.5.0 8 | certifi==2024.12.14 9 | charset-normalizer==3.4.1 10 | chroma-hnswlib==0.7.6 11 | chromadb==0.5.23 12 | click==8.1.8 13 | coloredlogs==15.0.1 14 | Deprecated==1.2.15 15 | durationpy==0.9 16 | fastapi==0.115.6 17 | filelock==3.16.1 18 | flatbuffers==24.12.23 19 | fsspec==2024.12.0 20 | google-auth==2.37.0 21 | googleapis-common-protos==1.66.0 22 | grpcio==1.69.0 23 | h11==0.14.0 24 | httpcore==1.0.7 25 | httptools==0.6.4 26 | httpx==0.28.1 27 | httpx-sse==0.4.0 28 | huggingface-hub==0.16.4 29 | humanfriendly==10.0 30 | idna==3.10 31 | importlib_metadata==8.5.0 32 | importlib_resources==6.5.2 33 | Jinja2==3.1.5 34 | joblib==1.4.2 35 | kubernetes==31.0.0 36 | markdown-it-py==3.0.0 37 | MarkupSafe==3.0.2 38 | mcp==1.2.0 39 | -e git+https://github.com/doobidoo/mcp-memory-service.git@1e8cce06ae8e82110a72cef3dda9dac2e108e720#egg=mcp_memory_service 40 | mdurl==0.1.2 41 | mmh3==5.0.1 42 | monotonic==1.6 43 | mpmath==1.3.0 44 | networkx==3.4.2 45 | nltk==3.9.1 46 | numpy==2.2.1 47 | oauthlib==3.2.2 48 | onnxruntime==1.20.1 49 | opentelemetry-api==1.29.0 50 | opentelemetry-exporter-otlp-proto-common==1.29.0 51 | opentelemetry-exporter-otlp-proto-grpc==1.29.0 52 | opentelemetry-instrumentation==0.50b0 53 | opentelemetry-instrumentation-asgi==0.50b0 54 | opentelemetry-instrumentation-fastapi==0.50b0 55 | opentelemetry-proto==1.29.0 56 | opentelemetry-sdk==1.29.0 57 | opentelemetry-semantic-conventions==0.50b0 58 | opentelemetry-util-http==0.50b0 59 | orjson==3.10.13 60 | overrides==7.7.0 61 | packaging==24.2 62 | pillow==11.1.0 63 | posthog==3.7.5 64 | protobuf==5.29.2 65 | pulsar-client==3.5.0 66 | pyasn1==0.6.1 67 | pyasn1_modules==0.4.1 68 | pydantic==2.10.4 69 | pydantic-settings==2.7.1 70 | pydantic_core==2.27.2 71 | Pygments==2.19.0 72 | PyPika==0.48.9 73 | pyproject_hooks==1.2.0 74 | python-dateutil==2.9.0.post0 75 | python-dotenv==1.0.1 76 | PyYAML==6.0.2 77 | regex==2024.11.6 78 | requests==2.32.3 79 | requests-oauthlib==2.0.0 80 | rich==13.9.4 81 | rsa==4.9 82 | safetensors==0.5.0 83 | scikit-learn==1.6.0 84 | scipy==1.15.0 85 | sentence-transformers==2.2.2 86 | sentencepiece==0.2.0 87 | shellingham==1.5.4 88 | six==1.17.0 89 | sniffio==1.3.1 90 | sse-starlette==2.2.1 91 | starlette==0.41.3 92 | sympy==1.13.1 93 | tenacity==9.0.0 94 | threadpoolctl==3.5.0 95 | tokenizers==0.13.3 96 | torch>=2.0.0 97 | torchvision==0.16.0 98 | torchaudio==2.1.0 99 | tqdm==4.67.1 100 | transformers==4.30.0 101 | typer==0.15.1 102 | typing_extensions==4.12.2 103 | urllib3==1.26.20 104 | uvicorn==0.34.0 105 | uvloop==0.21.0 106 | watchfiles==0.19.0 107 | websocket-client==1.8.0 108 | websockets==14.1 109 | wrapt==1.17.0 110 | zipp==3.21.0 111 | -------------------------------------------------------------------------------- /run-with-uv.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo Running MCP Memory Service with UV... 3 | python uv_wrapper.py %* 4 | -------------------------------------------------------------------------------- /run-with-uv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Running MCP Memory Service with UV..." 3 | python uv_wrapper.py "$@" 4 | -------------------------------------------------------------------------------- /scripts/TIME_BASED_RECALL_FIX.md: -------------------------------------------------------------------------------- 1 | # Time-Based Recall Fix for MCP Memory Service 2 | 3 | This guide explains how to fix the time-based recall functionality in the MCP Memory Service. The issue was identified in GitHub issue #19, where time-based recall queries were not working correctly due to inconsistent timestamp formats in the database. 4 | 5 | ## Understanding the Issue 6 | 7 | The key problems were: 8 | 9 | 1. Timestamps were being stored in different formats (floats, float strings, integer strings) 10 | 2. When querying, the formats didn't match which caused no results to be returned 11 | 3. The `recall_by_timeframe` tool was missing proper implementation 12 | 13 | ## Fix Implementation 14 | 15 | We've made the following changes to fix the issue: 16 | 17 | 1. Modified the `_format_metadata_for_chroma` method to store timestamps as integer timestamps 18 | 2. Updated the `recall` method to use integer timestamps in queries 19 | 3. Fixed the `handle_recall_by_timeframe` method implementation 20 | 4. Added proper logging to help diagnose any remaining issues 21 | 22 | ## Migration Process 23 | 24 | For the fix to work properly, all existing memories need to be updated to use the new timestamp format. We've created scripts to handle this migration safely: 25 | 26 | ### Step 1: Create a Backup 27 | 28 | Always start with a backup to ensure no data is lost: 29 | 30 | ```bash 31 | python scripts/backup_memories.py 32 | ``` 33 | 34 | This will create a backup file in your configured backups directory with timestamp in the filename. 35 | 36 | ### Step 2: Run the Migration Script 37 | 38 | The migration script updates all memory timestamps to the new format: 39 | 40 | ```bash 41 | python scripts/migrate_timestamps.py 42 | ``` 43 | 44 | **Note**: If you encounter any errors related to embeddings or array truth values, the scripts have been designed to be resilient and will use fallback methods. 45 | 46 | This script will: 47 | - Read all memories from the database 48 | - Convert their timestamps to integer strings 49 | - Update each memory in the database 50 | - Verify the migration was successful 51 | - Test a time-based query to ensure it works 52 | 53 | ### Step 3: Verify Time-Based Recall Works 54 | 55 | After running the migration, restart the MCP Memory Service and test time-based recall: 56 | 57 | ```bash 58 | # Using recall_memory 59 | mcp-memory-service.recall_memory({"query": "recall what I stored yesterday"}) 60 | 61 | # Using recall_by_timeframe 62 | mcp-memory-service.recall_by_timeframe({ 63 | "start_date": "2025-04-25", 64 | "end_date": "2025-04-26" 65 | }) 66 | ``` 67 | 68 | ## If Something Goes Wrong 69 | 70 | If the migration causes any issues, you can restore from your backup: 71 | 72 | ```bash 73 | python scripts/restore_memories.py memory_backup_YYYYMMDD_HHMMSS.json --reset 74 | ``` 75 | 76 | Use the `--reset` flag to completely reset the database before restoration. 77 | 78 | ## Technical Details 79 | 80 | ### New Timestamp Format 81 | 82 | - All timestamps are now stored as integer timestamps (not strings) 83 | - For example: `1714161600` instead of `1714161600.123` or `"1714161600"` 84 | - This ensures ChromaDB can properly compare timestamps in queries 85 | 86 | ### Affected Files 87 | 88 | - `src/mcp_memory_service/storage/chroma.py` 89 | - `src/mcp_memory_service/server.py` 90 | 91 | ### Testing 92 | 93 | After migration, you should be able to: 94 | 1. Create new memories and find them by time expressions 95 | 2. Recall older memories using time expressions 96 | 3. Use both natural language time expressions and explicit date ranges 97 | 98 | ## Summary 99 | 100 | This fix brings full time-based recall functionality to the MCP Memory Service, allowing you to retrieve memories using natural language time expressions like "yesterday," "last week," or specific date ranges. 101 | 102 | If you encounter any issues, please report them on the GitHub repository. 103 | -------------------------------------------------------------------------------- /scripts/backup_memories.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Backup script to export all memories from the database to a JSON file. 4 | This provides a safe backup before running migrations or making database changes. 5 | """ 6 | import sys 7 | import os 8 | import json 9 | import asyncio 10 | import logging 11 | import datetime 12 | from pathlib import Path 13 | 14 | # Add parent directory to path so we can import from the src directory 15 | sys.path.insert(0, str(Path(__file__).parent.parent)) 16 | 17 | from src.mcp_memory_service.storage.chroma import ChromaMemoryStorage 18 | from src.mcp_memory_service.config import CHROMA_PATH, BACKUPS_PATH 19 | 20 | # Configure logging 21 | logging.basicConfig( 22 | level=logging.INFO, 23 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 24 | ) 25 | logger = logging.getLogger("memory_backup") 26 | 27 | async def backup_memories(): 28 | """ 29 | Export all memories from the database to a JSON file. 30 | """ 31 | logger.info(f"Initializing ChromaDB storage at {CHROMA_PATH}") 32 | storage = ChromaMemoryStorage(CHROMA_PATH) 33 | 34 | # Create backups directory if it doesn't exist 35 | os.makedirs(BACKUPS_PATH, exist_ok=True) 36 | 37 | # Generate backup filename with timestamp 38 | timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 39 | backup_file = os.path.join(BACKUPS_PATH, f"memory_backup_{timestamp}.json") 40 | 41 | logger.info(f"Creating backup at {backup_file}") 42 | 43 | try: 44 | # Retrieve all memories from the database 45 | try: 46 | # First try with embeddings 47 | logger.info("Attempting to retrieve memories with embeddings") 48 | results = storage.collection.get( 49 | include=["metadatas", "documents"] 50 | ) 51 | include_embeddings = False 52 | except Exception as e: 53 | logger.warning(f"Failed to retrieve with embeddings: {e}") 54 | logger.info("Falling back to retrieving memories without embeddings") 55 | # Fall back to no embeddings 56 | results = storage.collection.get( 57 | include=["metadatas", "documents"] 58 | ) 59 | include_embeddings = False 60 | 61 | if not results["ids"]: 62 | logger.info("No memories found in database") 63 | return backup_file 64 | 65 | total_memories = len(results["ids"]) 66 | logger.info(f"Found {total_memories} memories to backup") 67 | 68 | # Create backup data structure 69 | backup_data = { 70 | "timestamp": datetime.datetime.now().isoformat(), 71 | "total_memories": total_memories, 72 | "memories": [] 73 | } 74 | 75 | # Process each memory 76 | for i, memory_id in enumerate(results["ids"]): 77 | metadata = results["metadatas"][i] 78 | document = results["documents"][i] 79 | embedding = None 80 | if include_embeddings and "embeddings" in results and results["embeddings"] is not None: 81 | if i < len(results["embeddings"]): 82 | embedding = results["embeddings"][i] 83 | 84 | memory_data = { 85 | "id": memory_id, 86 | "document": document, 87 | "metadata": metadata, 88 | "embedding": embedding 89 | } 90 | 91 | backup_data["memories"].append(memory_data) 92 | 93 | # Write backup to file 94 | with open(backup_file, 'w', encoding='utf-8') as f: 95 | json.dump(backup_data, f, indent=2, ensure_ascii=False) 96 | 97 | logger.info(f"Successfully backed up {total_memories} memories to {backup_file}") 98 | return backup_file 99 | 100 | except Exception as e: 101 | logger.error(f"Error creating backup: {str(e)}") 102 | raise 103 | 104 | async def main(): 105 | """Main function to run the backup.""" 106 | logger.info("=== Starting memory backup ===") 107 | 108 | try: 109 | backup_file = await backup_memories() 110 | logger.info(f"=== Backup completed successfully: {backup_file} ===") 111 | except Exception as e: 112 | logger.error(f"Backup failed: {str(e)}") 113 | sys.exit(1) 114 | 115 | if __name__ == "__main__": 116 | asyncio.run(main()) -------------------------------------------------------------------------------- /scripts/cleanup_memories.md: -------------------------------------------------------------------------------- 1 | # Memory Cleanup Script Documentation 2 | 3 | ## Overview 4 | 5 | The `cleanup_memories.py` script is designed to identify and remove erroneous entries from the ChromaDB database used by the MCP Memory Service. This tool is particularly useful for cleaning up corrupted memories without having to reset the entire database. 6 | 7 | ## Location 8 | 9 | This script should be placed in the `scripts` directory of your MCP Memory Service repository: 10 | 11 | ``` 12 | /Users/hkr/Documents/GitHub/mcp-memory-service/scripts/cleanup_memories.py 13 | ``` 14 | 15 | ## Features 16 | 17 | - Intelligently identifies potentially erroneous memory entries 18 | - Supports targeted deletion using text pattern matching 19 | - Provides a dry-run mode to preview changes before execution 20 | - Includes a full reset option as an alternative to targeted cleanup 21 | - Works with custom ChromaDB paths through environment variables 22 | 23 | ## Prerequisites 24 | 25 | - Python 3.7+ 26 | - Access to the MCP Memory Service codebase 27 | - Appropriate permissions to access and modify the ChromaDB directory 28 | 29 | ## Usage 30 | 31 | ### Basic Usage 32 | 33 | To run the script with default settings (which will attempt to identify common error patterns): 34 | 35 | ```bash 36 | cd /Users/hkr/Documents/GitHub/mcp-memory-service 37 | python scripts/cleanup_memories.py 38 | ``` 39 | 40 | ### Setting the ChromaDB Path 41 | 42 | The script uses the `MCP_MEMORY_CHROMA_PATH` environment variable to determine where the ChromaDB is located: 43 | 44 | ```bash 45 | export MCP_MEMORY_CHROMA_PATH="/path/to/your/chroma_db" 46 | python scripts/cleanup_memories.py 47 | ``` 48 | 49 | ### Available Options 50 | 51 | | Option | Description | 52 | |--------|-------------| 53 | | `--error-text TEXT` | Specify a text pattern found in erroneous entries | 54 | | `--dry-run` | Show what would be deleted without actually deleting | 55 | | `--reset` | Completely reset the database (use with caution!) | 56 | 57 | ### Example Commands 58 | 59 | #### Perform a Dry Run 60 | 61 | To preview what entries would be deleted without making any changes: 62 | 63 | ```bash 64 | python scripts/cleanup_memories.py --dry-run 65 | ``` 66 | 67 | #### Search for Specific Error Text 68 | 69 | To find and remove entries containing specific error text: 70 | 71 | ```bash 72 | python scripts/cleanup_memories.py --error-text "error pattern" 73 | ``` 74 | 75 | #### Reset the Entire Database 76 | 77 | To completely reset the database (equivalent to using the `--reset` flag with the restore script): 78 | 79 | ```bash 80 | python scripts/cleanup_memories.py --reset 81 | ``` 82 | 83 | ## How It Works 84 | 85 | 1. **Initialization**: The script connects to the ChromaDB at the specified location. 86 | 87 | 2. **Entry Analysis**: In the absence of a specific error pattern, the script looks for: 88 | - Very short entries (less than 10 characters) 89 | - Entries containing common error terms ('error', 'exception', 'failed', 'invalid') 90 | 91 | 3. **Cleanup Process**: 92 | - Identified entries are deleted in batches to avoid overwhelming the database 93 | - Progress and results are logged to the console 94 | 95 | ## Integration with Backup and Restore 96 | 97 | This script complements the existing `restore_memories.py` script: 98 | 99 | 1. First restore your memories from backup: 100 | ```bash 101 | python scripts/restore_memories.py "/path/to/backup.json" 102 | ``` 103 | 104 | 2. Then clean up any remaining erroneous entries: 105 | ```bash 106 | python scripts/cleanup_memories.py 107 | ``` 108 | 109 | ## Troubleshooting 110 | 111 | ### Script Can't Find ChromaDB 112 | 113 | If you encounter errors about the ChromaDB path: 114 | 115 | 1. Make sure you've set the `MCP_MEMORY_CHROMA_PATH` environment variable correctly 116 | 2. Verify that the path exists and is accessible 117 | 3. Check permissions on the ChromaDB directory 118 | 119 | ### No Erroneous Entries Found 120 | 121 | If the script reports "No erroneous entries found" but you know there are problems: 122 | 123 | 1. Try specifying an `--error-text` parameter with a pattern you know exists in the bad entries 124 | 2. Check if your ChromaDB path is correct 125 | 3. Run a query directly against the database to confirm the presence of problematic entries 126 | 127 | ## Caution 128 | 129 | - Always run with `--dry-run` first to preview changes 130 | - Consider making a backup of your ChromaDB directory before running cleanup operations 131 | - The `--reset` option will delete ALL memories in the database 132 | 133 | ## Logging 134 | 135 | The script provides detailed logging to help track the cleanup process: 136 | - Information about the total number of memories found 137 | - Details about identified erroneous entries 138 | - Progress updates during batch deletion 139 | - Errors and warnings when issues are encountered 140 | -------------------------------------------------------------------------------- /scripts/cleanup_memories.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Script to clean up erroneous memory entries from ChromaDB. 4 | """ 5 | import sys 6 | import os 7 | import asyncio 8 | import logging 9 | import argparse 10 | from pathlib import Path 11 | 12 | # Add parent directory to path so we can import from the src directory 13 | sys.path.insert(0, str(Path(__file__).parent.parent)) 14 | 15 | from src.mcp_memory_service.storage.chroma import ChromaMemoryStorage 16 | from src.mcp_memory_service.config import CHROMA_PATH 17 | 18 | # Configure logging 19 | logging.basicConfig( 20 | level=logging.INFO, 21 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 22 | ) 23 | logger = logging.getLogger("memory_cleanup") 24 | 25 | def parse_args(): 26 | """Parse command line arguments.""" 27 | parser = argparse.ArgumentParser(description="Clean up erroneous memory entries") 28 | parser.add_argument("--error-text", help="Text pattern found in erroneous entries", type=str) 29 | parser.add_argument("--dry-run", action="store_true", help="Show what would be deleted without actually deleting") 30 | parser.add_argument("--reset", action="store_true", help="Completely reset the database (use with caution!)") 31 | return parser.parse_args() 32 | 33 | async def cleanup_memories(error_text=None, dry_run=False, reset=False): 34 | """ 35 | Clean up erroneous memory entries from ChromaDB. 36 | 37 | Args: 38 | error_text: Text pattern found in erroneous entries 39 | dry_run: If True, only show what would be deleted without actually deleting 40 | reset: If True, completely reset the database 41 | """ 42 | logger.info(f"Initializing ChromaDB storage at {CHROMA_PATH}") 43 | storage = ChromaMemoryStorage(CHROMA_PATH) 44 | 45 | if reset: 46 | if dry_run: 47 | logger.warning("[DRY RUN] Would reset the entire database") 48 | else: 49 | logger.warning("Resetting the entire database") 50 | try: 51 | storage.client.delete_collection("memory_collection") 52 | logger.info("Deleted existing collection") 53 | 54 | # Reinitialize collection 55 | storage.collection = storage.client.create_collection( 56 | name="memory_collection", 57 | metadata={"hnsw:space": "cosine"}, 58 | embedding_function=storage.embedding_function 59 | ) 60 | logger.info("Created new empty collection") 61 | except Exception as e: 62 | logger.error(f"Error resetting collection: {str(e)}") 63 | return 64 | 65 | # Get all memory entries 66 | try: 67 | # Query all entries 68 | result = storage.collection.get() 69 | total_memories = len(result['ids']) if 'ids' in result else 0 70 | logger.info(f"Found {total_memories} total memories in the database") 71 | 72 | if total_memories == 0: 73 | logger.info("No memories found in the database") 74 | return 75 | 76 | # Find erroneous entries 77 | error_ids = [] 78 | 79 | if error_text: 80 | logger.info(f"Searching for entries containing text pattern: '{error_text}'") 81 | for i, doc in enumerate(result['documents']): 82 | if error_text in doc: 83 | error_ids.append(result['ids'][i]) 84 | if len(error_ids) <= 5: # Show a few examples 85 | logger.info(f"Found erroneous entry: {doc[:100]}...") 86 | 87 | # If no specific error text, look for common error patterns 88 | if not error_text and not error_ids: 89 | logger.info("No specific error text provided, looking for common error patterns") 90 | for i, doc in enumerate(result['documents']): 91 | # Look for very short documents (likely errors) 92 | if len(doc.strip()) < 10: 93 | error_ids.append(result['ids'][i]) 94 | logger.info(f"Found suspiciously short entry: '{doc}'") 95 | # Look for error messages 96 | elif any(err in doc.lower() for err in ['error', 'exception', 'failed', 'invalid']): 97 | error_ids.append(result['ids'][i]) 98 | if len(error_ids) <= 5: # Show a few examples 99 | logger.info(f"Found likely error entry: {doc[:100]}...") 100 | 101 | if not error_ids: 102 | logger.info("No erroneous entries found") 103 | return 104 | 105 | logger.info(f"Found {len(error_ids)} erroneous entries") 106 | 107 | # Delete erroneous entries 108 | if dry_run: 109 | logger.info(f"[DRY RUN] Would delete {len(error_ids)} erroneous entries") 110 | else: 111 | logger.info(f"Deleting {len(error_ids)} erroneous entries") 112 | # Process in batches to avoid overwhelming the database 113 | batch_size = 100 114 | for i in range(0, len(error_ids), batch_size): 115 | batch = error_ids[i:i+batch_size] 116 | logger.info(f"Deleting batch {i//batch_size + 1}/{(len(error_ids)-1)//batch_size + 1}") 117 | storage.collection.delete(ids=batch) 118 | 119 | logger.info("Deletion completed") 120 | 121 | except Exception as e: 122 | logger.error(f"Error cleaning up memories: {str(e)}") 123 | raise 124 | 125 | async def main(): 126 | """Main function to run the cleanup.""" 127 | args = parse_args() 128 | 129 | logger.info("=== Starting memory cleanup ===") 130 | 131 | try: 132 | await cleanup_memories(args.error_text, args.dry_run, args.reset) 133 | logger.info("=== Cleanup completed successfully ===") 134 | except Exception as e: 135 | logger.error(f"Cleanup failed: {str(e)}") 136 | sys.exit(1) 137 | 138 | if __name__ == "__main__": 139 | asyncio.run(main()) 140 | -------------------------------------------------------------------------------- /scripts/fix_readline.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Fix script for readline issues in the Python interactive shell. 4 | This script updates the sitecustomize.py file to handle readline properly. 5 | """ 6 | import os 7 | import sys 8 | import site 9 | import shutil 10 | 11 | def print_info(text): 12 | """Print formatted info text.""" 13 | print(f"[INFO] {text}") 14 | 15 | def print_error(text): 16 | """Print formatted error text.""" 17 | print(f"[ERROR] {text}") 18 | 19 | def print_success(text): 20 | """Print formatted success text.""" 21 | print(f"[SUCCESS] {text}") 22 | 23 | def print_warning(text): 24 | """Print formatted warning text.""" 25 | print(f"[WARNING] {text}") 26 | 27 | def fix_sitecustomize_readline(): 28 | """Fix the sitecustomize.py file to handle readline properly.""" 29 | # Get site-packages directory 30 | site_packages = site.getsitepackages()[0] 31 | 32 | # Path to sitecustomize.py 33 | sitecustomize_path = os.path.join(site_packages, 'sitecustomize.py') 34 | 35 | # Check if file exists 36 | if not os.path.exists(sitecustomize_path): 37 | print_error(f"sitecustomize.py not found at {sitecustomize_path}") 38 | return False 39 | 40 | # Create backup if it doesn't exist 41 | backup_path = sitecustomize_path + '.readline.bak' 42 | if not os.path.exists(backup_path): 43 | print_info(f"Creating backup of sitecustomize.py at {backup_path}") 44 | shutil.copy2(sitecustomize_path, backup_path) 45 | print_success(f"Backup created at {backup_path}") 46 | else: 47 | print_warning(f"Backup already exists at {backup_path}") 48 | 49 | # Read the current content 50 | with open(sitecustomize_path, 'r') as f: 51 | content = f.read() 52 | 53 | # Add readline fix 54 | readline_fix = """ 55 | # Fix for readline module in interactive shell 56 | try: 57 | import readline 58 | # Check if we're in interactive mode 59 | if hasattr(sys, 'ps1'): 60 | try: 61 | # Only call register_readline if it exists and we're in interactive mode 62 | if hasattr(sys, '__interactivehook__'): 63 | # Patch readline.backend if it doesn't exist 64 | if not hasattr(readline, 'backend'): 65 | readline.backend = 'readline' 66 | except Exception as e: 67 | print(f"Warning: Readline initialization error: {e}", file=sys.stderr) 68 | except ImportError: 69 | # Readline not available, skip 70 | pass 71 | """ 72 | 73 | # Check if the fix is already in the file 74 | if "Fix for readline module in interactive shell" in content: 75 | print_info("Readline fix already present in sitecustomize.py") 76 | return True 77 | 78 | # Add the fix at the beginning of the file 79 | new_content = readline_fix + content 80 | 81 | # Write the updated content 82 | with open(sitecustomize_path, 'w') as f: 83 | f.write(new_content) 84 | 85 | print_success(f"Added readline fix to {sitecustomize_path}") 86 | return True 87 | 88 | def main(): 89 | """Main function.""" 90 | print_info("Fixing sitecustomize.py to handle readline properly") 91 | 92 | if fix_sitecustomize_readline(): 93 | print_success("sitecustomize.py fixed successfully for readline") 94 | else: 95 | print_error("Failed to fix sitecustomize.py for readline") 96 | sys.exit(1) 97 | 98 | if __name__ == "__main__": 99 | main() -------------------------------------------------------------------------------- /scripts/install_uv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Script to install UV package manager 4 | """ 5 | import os 6 | import sys 7 | import subprocess 8 | import platform 9 | 10 | def main(): 11 | print("Installing UV package manager...") 12 | 13 | try: 14 | # Install UV using pip 15 | subprocess.check_call([ 16 | sys.executable, '-m', 'pip', 'install', 'uv' 17 | ]) 18 | 19 | print("UV installed successfully!") 20 | print("You can now use UV for faster dependency management:") 21 | print(" uv pip install -r requirements.txt") 22 | 23 | # Create shortcut script 24 | system = platform.system().lower() 25 | if system == "windows": 26 | # Create .bat file for Windows 27 | with open("uv-run.bat", "w") as f: 28 | f.write(f"@echo off\n") 29 | f.write(f"python -m uv run memory %*\n") 30 | print("Created uv-run.bat shortcut") 31 | else: 32 | # Create shell script for Unix-like systems 33 | with open("uv-run.sh", "w") as f: 34 | f.write("#!/bin/sh\n") 35 | f.write("python -m uv run memory \"$@\"\n") 36 | 37 | # Make it executable 38 | try: 39 | os.chmod("uv-run.sh", 0o755) 40 | except: 41 | pass 42 | print("Created uv-run.sh shortcut") 43 | 44 | except subprocess.SubprocessError as e: 45 | print(f"Error installing UV: {e}") 46 | sys.exit(1) 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /scripts/list-collections.py: -------------------------------------------------------------------------------- 1 | from chromadb import HttpClient 2 | 3 | def list_collections(): 4 | try: 5 | # Connect to local ChromaDB 6 | client = HttpClient(host='localhost', port=8000) 7 | 8 | # List all collections 9 | collections = client.list_collections() 10 | 11 | print("\nFound Collections:") 12 | print("------------------") 13 | for collection in collections: 14 | print(f"Name: {collection.name}") 15 | print(f"Metadata: {collection.metadata}") 16 | print(f"Count: {collection.count()}") 17 | print("------------------") 18 | 19 | except Exception as e: 20 | print(f"Error connecting to local ChromaDB: {str(e)}") 21 | 22 | if __name__ == "__main__": 23 | list_collections() 24 | -------------------------------------------------------------------------------- /scripts/mcp-migration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Enhanced migration script for MCP Memory Service. 4 | This script handles migration of memories between different ChromaDB instances, 5 | with support for both local and remote migrations. 6 | """ 7 | import sys 8 | import os 9 | from dotenv import load_dotenv 10 | from pathlib import Path 11 | import chromadb 12 | from chromadb import HttpClient, Settings 13 | import json 14 | import time 15 | from chromadb.utils import embedding_functions 16 | 17 | # Import our environment verifier 18 | from verify_environment import EnvironmentVerifier 19 | 20 | def verify_environment(): 21 | """Verify the environment before proceeding with migration""" 22 | verifier = EnvironmentVerifier() 23 | verifier.run_verifications() 24 | if not verifier.print_results(): 25 | print("\n⚠️ Environment verification failed! Migration cannot proceed.") 26 | sys.exit(1) 27 | print("\n✓ Environment verification passed! Proceeding with migration.") 28 | 29 | # Load environment variables 30 | load_dotenv() 31 | 32 | def get_claude_desktop_chroma_path(): 33 | """Get ChromaDB path from Claude Desktop config""" 34 | base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 35 | config_path = os.path.join(base_path, 'claude_config', 'mcp-memory', 'chroma_db') 36 | print(f"Using ChromaDB path: {config_path}") 37 | return config_path 38 | 39 | def migrate_memories(source_type, source_config, target_type, target_config): 40 | """ 41 | Migrate memories between ChromaDB instances. 42 | 43 | Args: 44 | source_type: 'local' or 'remote' 45 | source_config: For local: path to ChromaDB, for remote: {'host': host, 'port': port} 46 | target_type: 'local' or 'remote' 47 | target_config: For local: path to ChromaDB, for remote: {'host': host, 'port': port} 48 | """ 49 | print(f"Starting migration from {source_type} to {target_type}") 50 | 51 | try: 52 | # Set up embedding function 53 | embedding_function = embedding_functions.SentenceTransformerEmbeddingFunction( 54 | model_name='all-MiniLM-L6-v2' 55 | ) 56 | 57 | # Connect to target ChromaDB 58 | if target_type == 'remote': 59 | target_client = HttpClient( 60 | host=target_config['host'], 61 | port=target_config['port'] 62 | ) 63 | print(f"Connected to remote ChromaDB at {target_config['host']}:{target_config['port']}") 64 | else: 65 | settings = Settings( 66 | anonymized_telemetry=False, 67 | allow_reset=True, 68 | is_persistent=True, 69 | persist_directory=target_config 70 | ) 71 | target_client = chromadb.Client(settings) 72 | print(f"Connected to local ChromaDB at {target_config}") 73 | 74 | # Get or create collection for imported memories 75 | try: 76 | target_collection = target_client.get_collection( 77 | name="mcp_imported_memories", 78 | embedding_function=embedding_function 79 | ) 80 | print("Found existing collection 'mcp_imported_memories' on target") 81 | except Exception: 82 | target_collection = target_client.create_collection( 83 | name="mcp_imported_memories", 84 | metadata={"hnsw:space": "cosine"}, 85 | embedding_function=embedding_function 86 | ) 87 | print("Created new collection 'mcp_imported_memories' on target") 88 | 89 | # Connect to source ChromaDB 90 | if source_type == 'remote': 91 | source_client = HttpClient( 92 | host=source_config['host'], 93 | port=source_config['port'] 94 | ) 95 | print(f"Connected to remote ChromaDB at {source_config['host']}:{source_config['port']}") 96 | else: 97 | settings = Settings( 98 | anonymized_telemetry=False, 99 | allow_reset=True, 100 | is_persistent=True, 101 | persist_directory=source_config 102 | ) 103 | source_client = chromadb.Client(settings) 104 | print(f"Connected to local ChromaDB at {source_config}") 105 | 106 | # List collections 107 | collections = source_client.list_collections() 108 | print(f"Found {len(collections)} collections in source") 109 | for coll in collections: 110 | print(f"- {coll.name}") 111 | 112 | # Try to get the memory collection 113 | try: 114 | source_collection = source_client.get_collection( 115 | name="memory_collection", 116 | embedding_function=embedding_function 117 | ) 118 | print("Found source memory collection") 119 | except ValueError as e: 120 | print(f"Error accessing source collection: {str(e)}") 121 | return 122 | 123 | # Get all memories from source 124 | print("Fetching source memories...") 125 | results = source_collection.get() 126 | 127 | if not results["ids"]: 128 | print("No memories found in source collection") 129 | return 130 | 131 | print(f"Found {len(results['ids'])} memories to migrate") 132 | 133 | # Check for existing memories in target to avoid duplicates 134 | target_existing = target_collection.get() 135 | existing_ids = set(target_existing["ids"]) 136 | 137 | # Filter out already migrated memories 138 | new_memories = { 139 | "ids": [], 140 | "documents": [], 141 | "metadatas": [] 142 | } 143 | 144 | for i, memory_id in enumerate(results["ids"]): 145 | if memory_id not in existing_ids: 146 | new_memories["ids"].append(memory_id) 147 | new_memories["documents"].append(results["documents"][i]) 148 | new_memories["metadatas"].append(results["metadatas"][i]) 149 | 150 | if not new_memories["ids"]: 151 | print("All memories are already migrated!") 152 | return 153 | 154 | print(f"Found {len(new_memories['ids'])} new memories to migrate") 155 | 156 | # Import in batches of 10 157 | batch_size = 10 158 | for i in range(0, len(new_memories['ids']), batch_size): 159 | batch_end = min(i + batch_size, len(new_memories['ids'])) 160 | 161 | batch_ids = new_memories['ids'][i:batch_end] 162 | batch_documents = new_memories['documents'][i:batch_end] 163 | batch_metadatas = new_memories['metadatas'][i:batch_end] 164 | 165 | print(f"Migrating batch {i//batch_size + 1} ({len(batch_ids)} memories)...") 166 | 167 | target_collection.add( 168 | documents=batch_documents, 169 | metadatas=batch_metadatas, 170 | ids=batch_ids 171 | ) 172 | 173 | # Small delay between batches 174 | time.sleep(1) 175 | 176 | print("\nMigration complete!") 177 | 178 | # Verify migration 179 | target_results = target_collection.get() 180 | print(f"Verification: {len(target_results['ids'])} total memories in target collection") 181 | 182 | except Exception as e: 183 | print(f"Error during migration: {str(e)}") 184 | print("Please ensure both ChromaDB instances are running and accessible") 185 | 186 | if __name__ == "__main__": 187 | # First verify the environment 188 | verify_environment() 189 | 190 | # Example usage: 191 | # Local to remote migration 192 | migrate_memories( 193 | source_type='local', 194 | source_config=get_claude_desktop_chroma_path(), 195 | target_type='remote', 196 | target_config={'host': '16.171.169.46', 'port': 8000} 197 | ) 198 | 199 | # Remote to local migration 200 | # migrate_memories( 201 | # source_type='remote', 202 | # source_config={'host': '16.171.169.46', 'port': 8000}, 203 | # target_type='local', 204 | # target_config=get_claude_desktop_chroma_path() 205 | # ) 206 | -------------------------------------------------------------------------------- /scripts/migrate_tags.py: -------------------------------------------------------------------------------- 1 | # scripts/migrate_tags.py 2 | # python scripts/validate_memories.py --db-path /path/to/your/chroma_db 3 | 4 | import asyncio 5 | import json 6 | import logging 7 | from datetime import datetime 8 | from pathlib import Path 9 | from mcp_memory_service.storage.chroma import ChromaMemoryStorage 10 | import argparse 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | async def analyze_tag_formats(metadatas): 15 | """Analyze the current tag formats in the database""" 16 | formats = { 17 | "json_string": 0, 18 | "raw_list": 0, 19 | "comma_string": 0, 20 | "empty": 0, 21 | "invalid": 0 22 | } 23 | 24 | for meta in metadatas: 25 | tags = meta.get("tags") 26 | if tags is None: 27 | formats["empty"] += 1 28 | continue 29 | 30 | if isinstance(tags, list): 31 | formats["raw_list"] += 1 32 | elif isinstance(tags, str): 33 | try: 34 | parsed = json.loads(tags) 35 | if isinstance(parsed, list): 36 | formats["json_string"] += 1 37 | else: 38 | formats["invalid"] += 1 39 | except json.JSONDecodeError: 40 | if "," in tags: 41 | formats["comma_string"] += 1 42 | else: 43 | formats["invalid"] += 1 44 | else: 45 | formats["invalid"] += 1 46 | 47 | return formats 48 | 49 | async def find_invalid_tags(metadatas): 50 | """Find any invalid tag formats""" 51 | invalid_entries = [] 52 | 53 | for i, meta in enumerate(metadatas): 54 | tags = meta.get("tags") 55 | if tags is None: 56 | continue 57 | 58 | try: 59 | if isinstance(tags, str): 60 | json.loads(tags) 61 | except json.JSONDecodeError: 62 | invalid_entries.append({ 63 | "memory_id": meta.get("content_hash", f"index_{i}"), 64 | "tags": tags 65 | }) 66 | 67 | return invalid_entries 68 | 69 | async def backup_memories(storage): 70 | """Create a backup of all memories""" 71 | results = storage.collection.get(include=["metadatas", "documents"]) 72 | 73 | backup_data = { 74 | "timestamp": datetime.now().isoformat(), 75 | "memories": [{ 76 | "id": results["ids"][i], 77 | "content": results["documents"][i], 78 | "metadata": results["metadatas"][i] 79 | } for i in range(len(results["ids"]))] 80 | } 81 | 82 | backup_path = Path("backups") 83 | backup_path.mkdir(exist_ok=True) 84 | 85 | backup_file = backup_path / f"memory_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" 86 | with open(backup_file, 'w') as f: 87 | json.dump(backup_data, f) 88 | 89 | return backup_file 90 | 91 | async def validate_current_state(storage): 92 | """Validate the current state of the database""" 93 | results = storage.collection.get(include=["metadatas"]) 94 | return { 95 | "total_memories": len(results["ids"]), 96 | "tag_formats": await analyze_tag_formats(results["metadatas"]), 97 | "invalid_tags": await find_invalid_tags(results["metadatas"]) 98 | } 99 | 100 | async def migrate_tags(storage): 101 | """Perform the tag migration""" 102 | results = storage.collection.get(include=["metadatas", "documents"]) 103 | 104 | migrated_count = 0 105 | error_count = 0 106 | 107 | for i, meta in enumerate(results["metadatas"]): 108 | try: 109 | # Extract current tags 110 | current_tags = meta.get("tags", "[]") 111 | 112 | # Normalize to list format 113 | if isinstance(current_tags, str): 114 | try: 115 | # Try parsing as JSON first 116 | tags = json.loads(current_tags) 117 | if isinstance(tags, str): 118 | tags = [t.strip() for t in tags.split(",")] 119 | elif isinstance(tags, list): 120 | tags = [str(t).strip() for t in tags] 121 | else: 122 | tags = [] 123 | except json.JSONDecodeError: 124 | # Handle as comma-separated string 125 | tags = [t.strip() for t in current_tags.split(",")] 126 | elif isinstance(current_tags, list): 127 | tags = [str(t).strip() for t in current_tags] 128 | else: 129 | tags = [] 130 | 131 | # Update with normalized format 132 | new_meta = meta.copy() 133 | new_meta["tags"] = json.dumps(tags) 134 | 135 | # Update memory 136 | storage.collection.update( 137 | ids=[results["ids"][i]], 138 | metadatas=[new_meta] 139 | ) 140 | 141 | migrated_count += 1 142 | 143 | except Exception as e: 144 | error_count += 1 145 | logger.error(f"Error migrating memory {results['ids'][i]}: {str(e)}") 146 | 147 | return migrated_count, error_count 148 | 149 | async def verify_migration(storage): 150 | """Verify the migration was successful""" 151 | results = storage.collection.get(include=["metadatas"]) 152 | 153 | verification = { 154 | "total_memories": len(results["ids"]), 155 | "tag_formats": await analyze_tag_formats(results["metadatas"]), 156 | "invalid_tags": await find_invalid_tags(results["metadatas"]) 157 | } 158 | 159 | return verification 160 | 161 | async def rollback_migration(storage, backup_file): 162 | """Rollback to the backup if needed""" 163 | with open(backup_file, 'r') as f: 164 | backup = json.load(f) 165 | 166 | for memory in backup["memories"]: 167 | storage.collection.update( 168 | ids=[memory["id"]], 169 | metadatas=[memory["metadata"]], 170 | documents=[memory["content"]] 171 | ) 172 | 173 | async def main(): 174 | # Configure logging 175 | log_level = os.getenv('LOG_LEVEL', 'ERROR').upper() 176 | logging.basicConfig( 177 | level=getattr(logging, log_level, logging.ERROR), 178 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 179 | stream=sys.stderr 180 | ) 181 | 182 | # Initialize storage 183 | # storage = ChromaMemoryStorage("path/to/your/db") 184 | 185 | # Parse command line arguments 186 | parser = argparse.ArgumentParser(description='Validate memory data tags') 187 | parser.add_argument('--db-path', required=True, help='Path to ChromaDB database') 188 | args = parser.parse_args() 189 | 190 | # Initialize storage with provided path 191 | logger.info(f"Connecting to database at: {args.db_path}") 192 | storage = ChromaMemoryStorage(args.db_path) 193 | 194 | 195 | # 1. Create backup 196 | logger.info("Creating backup...") 197 | backup_file = await backup_memories(storage) 198 | logger.info(f"Backup created at: {backup_file}") 199 | 200 | # 2. Validate current state 201 | logger.info("Validating current state...") 202 | current_state = await validate_current_state(storage) 203 | logger.info("\nCurrent state:") 204 | logger.info(json.dumps(current_state, indent=2)) 205 | 206 | # 3. Confirm migration 207 | proceed = input("\nProceed with migration? (yes/no): ") 208 | if proceed.lower() == 'yes': 209 | # 4. Run migration 210 | logger.info("Running migration...") 211 | migrated_count, error_count = await migrate_tags(storage) 212 | logger.info(f"Migration completed. Migrated: {migrated_count}, Errors: {error_count}") 213 | 214 | # 5. Verify migration 215 | logger.info("Verifying migration...") 216 | verification = await verify_migration(storage) 217 | logger.info("\nMigration verification:") 218 | logger.info(json.dumps(verification, indent=2)) 219 | 220 | # 6. Check if rollback needed 221 | if error_count > 0: 222 | rollback = input("\nErrors detected. Rollback to backup? (yes/no): ") 223 | if rollback.lower() == 'yes': 224 | logger.info("Rolling back...") 225 | await rollback_migration(storage, backup_file) 226 | logger.info("Rollback completed") 227 | else: 228 | logger.info("Migration cancelled") 229 | 230 | if __name__ == "__main__": 231 | asyncio.run(main()) 232 | -------------------------------------------------------------------------------- /scripts/repair_memories.py: -------------------------------------------------------------------------------- 1 | # scripts/repair_memories.py 2 | 3 | import asyncio 4 | import json 5 | import logging 6 | from mcp_memory_service.storage.chroma import ChromaMemoryStorage 7 | from mcp_memory_service.utils.hashing import generate_content_hash 8 | import argparse 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | async def repair_missing_hashes(storage): 13 | """Repair memories missing content_hash by generating new ones""" 14 | results = storage.collection.get( 15 | include=["metadatas", "documents"] 16 | ) 17 | 18 | fixed_count = 0 19 | for i, meta in enumerate(results["metadatas"]): 20 | memory_id = results["ids"][i] 21 | 22 | if "content_hash" not in meta: 23 | try: 24 | # Generate hash from content and metadata 25 | content = results["documents"][i] 26 | # Create a copy of metadata without the content_hash field 27 | meta_for_hash = {k: v for k, v in meta.items() if k != "content_hash"} 28 | new_hash = generate_content_hash(content, meta_for_hash) 29 | 30 | # Update metadata with new hash 31 | new_meta = meta.copy() 32 | new_meta["content_hash"] = new_hash 33 | 34 | # Update the memory 35 | storage.collection.update( 36 | ids=[memory_id], 37 | metadatas=[new_meta] 38 | ) 39 | 40 | logger.info(f"Fixed memory {memory_id} with new hash: {new_hash}") 41 | fixed_count += 1 42 | 43 | except Exception as e: 44 | logger.error(f"Error fixing memory {memory_id}: {str(e)}") 45 | 46 | return fixed_count 47 | 48 | async def main(): 49 | log_level = os.getenv('LOG_LEVEL', 'ERROR').upper() 50 | logging.basicConfig( 51 | level=getattr(logging, log_level, logging.ERROR), 52 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 53 | stream=sys.stderr 54 | ) 55 | 56 | parser = argparse.ArgumentParser(description='Repair memories with missing content hashes') 57 | parser.add_argument('--db-path', required=True, help='Path to ChromaDB database') 58 | args = parser.parse_args() 59 | 60 | logger.info(f"Connecting to database at: {args.db_path}") 61 | storage = ChromaMemoryStorage(args.db_path) 62 | 63 | logger.info("Starting repair process...") 64 | fixed_count = await repair_missing_hashes(storage) 65 | logger.info(f"Repair completed. Fixed {fixed_count} memories") 66 | 67 | # Run validation again to confirm fixes 68 | logger.info("Running validation to confirm fixes...") 69 | from validate_memories import run_validation_report 70 | report = await run_validation_report(storage) 71 | print("\nPost-repair validation report:") 72 | print(report) 73 | 74 | if __name__ == "__main__": 75 | asyncio.run(main()) -------------------------------------------------------------------------------- /scripts/requirements-migration.txt: -------------------------------------------------------------------------------- 1 | # chromadb==0.4.13 # Keep your working version 2 | # sentence-transformers==2.2.2 3 | # urllib3<2.0.0 # Add this to fix the SSL cipher issue 4 | chromadb 5 | sentence-transformers 6 | urllib3 -------------------------------------------------------------------------------- /scripts/restore_memories.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Restoration script to import memories from a backup JSON file into the database. 4 | This can be used to restore memories after a database issue or migration problem. 5 | """ 6 | import sys 7 | import os 8 | import json 9 | import asyncio 10 | import logging 11 | import argparse 12 | from pathlib import Path 13 | 14 | # Add parent directory to path so we can import from the src directory 15 | sys.path.insert(0, str(Path(__file__).parent.parent)) 16 | 17 | from src.mcp_memory_service.storage.chroma import ChromaMemoryStorage 18 | from src.mcp_memory_service.config import CHROMA_PATH, BACKUPS_PATH 19 | 20 | # Configure logging 21 | logging.basicConfig( 22 | level=logging.INFO, 23 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 24 | ) 25 | logger = logging.getLogger("memory_restore") 26 | 27 | def parse_args(): 28 | """Parse command line arguments.""" 29 | parser = argparse.ArgumentParser(description="Restore memories from backup file") 30 | parser.add_argument("backup_file", help="Path to backup JSON file", type=str) 31 | parser.add_argument("--reset", action="store_true", help="Reset database before restoration") 32 | return parser.parse_args() 33 | 34 | async def restore_memories(backup_file, reset_db=False): 35 | """ 36 | Import memories from a backup JSON file into the database. 37 | 38 | Args: 39 | backup_file: Path to the backup JSON file 40 | reset_db: If True, reset the database before restoration 41 | """ 42 | logger.info(f"Initializing ChromaDB storage at {CHROMA_PATH}") 43 | storage = ChromaMemoryStorage(CHROMA_PATH) 44 | 45 | # Check if backup file exists 46 | if not os.path.exists(backup_file): 47 | # Check if it's a filename in the backups directory 48 | potential_path = os.path.join(BACKUPS_PATH, backup_file) 49 | if os.path.exists(potential_path): 50 | backup_file = potential_path 51 | else: 52 | raise FileNotFoundError(f"Backup file not found: {backup_file}") 53 | 54 | logger.info(f"Loading backup from {backup_file}") 55 | 56 | try: 57 | # Load backup data 58 | with open(backup_file, 'r', encoding='utf-8') as f: 59 | backup_data = json.load(f) 60 | 61 | total_memories = backup_data.get("total_memories", 0) 62 | memories = backup_data.get("memories", []) 63 | 64 | if not memories: 65 | logger.warning("No memories found in backup file") 66 | return 67 | 68 | logger.info(f"Found {len(memories)} memories in backup file") 69 | 70 | # Reset database if requested 71 | if reset_db: 72 | logger.warning("Resetting database before restoration") 73 | try: 74 | storage.client.delete_collection("memory_collection") 75 | logger.info("Deleted existing collection") 76 | except Exception as e: 77 | logger.error(f"Error deleting collection: {str(e)}") 78 | 79 | # Reinitialize collection 80 | storage.collection = storage.client.create_collection( 81 | name="memory_collection", 82 | metadata={"hnsw:space": "cosine"}, 83 | embedding_function=storage.embedding_function 84 | ) 85 | logger.info("Created new collection") 86 | 87 | # Process memories in batches 88 | batch_size = 50 89 | success_count = 0 90 | error_count = 0 91 | 92 | for i in range(0, len(memories), batch_size): 93 | batch = memories[i:i+batch_size] 94 | logger.info(f"Processing batch {i//batch_size + 1}/{(len(memories)-1)//batch_size + 1}") 95 | 96 | # Prepare batch data 97 | batch_ids = [] 98 | batch_documents = [] 99 | batch_metadatas = [] 100 | batch_embeddings = [] 101 | 102 | for memory in batch: 103 | batch_ids.append(memory["id"]) 104 | batch_documents.append(memory["document"]) 105 | batch_metadatas.append(memory["metadata"]) 106 | if memory.get("embedding") is not None: 107 | batch_embeddings.append(memory["embedding"]) 108 | 109 | try: 110 | # Use upsert to avoid duplicates 111 | if batch_embeddings and len(batch_embeddings) > 0 and len(batch_embeddings) == len(batch_ids): 112 | storage.collection.upsert( 113 | ids=batch_ids, 114 | documents=batch_documents, 115 | metadatas=batch_metadatas, 116 | embeddings=batch_embeddings 117 | ) 118 | else: 119 | storage.collection.upsert( 120 | ids=batch_ids, 121 | documents=batch_documents, 122 | metadatas=batch_metadatas 123 | ) 124 | success_count += len(batch) 125 | except Exception as e: 126 | logger.error(f"Error restoring batch: {str(e)}") 127 | error_count += len(batch) 128 | 129 | logger.info(f"Restoration completed: {success_count} memories restored, {error_count} errors") 130 | 131 | except Exception as e: 132 | logger.error(f"Error restoring backup: {str(e)}") 133 | raise 134 | 135 | async def main(): 136 | """Main function to run the restoration.""" 137 | args = parse_args() 138 | 139 | logger.info("=== Starting memory restoration ===") 140 | 141 | try: 142 | await restore_memories(args.backup_file, args.reset) 143 | logger.info("=== Restoration completed successfully ===") 144 | except Exception as e: 145 | logger.error(f"Restoration failed: {str(e)}") 146 | sys.exit(1) 147 | 148 | if __name__ == "__main__": 149 | asyncio.run(main()) -------------------------------------------------------------------------------- /scripts/run_memory_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Direct runner for MCP Memory Service. 4 | This script directly imports and runs the memory server without going through the installation process. 5 | """ 6 | import os 7 | import sys 8 | import importlib.util 9 | import importlib.machinery 10 | import traceback 11 | 12 | # Disable sitecustomize.py and other import hooks to prevent recursion issues 13 | os.environ["PYTHONNOUSERSITE"] = "1" # Disable user site-packages 14 | os.environ["PYTHONPATH"] = "" # Clear PYTHONPATH 15 | 16 | # Set environment variables to prevent pip from installing dependencies 17 | os.environ["PIP_NO_DEPENDENCIES"] = "1" 18 | os.environ["PIP_NO_INSTALL"] = "1" 19 | 20 | # Set environment variables for better cross-platform compatibility 21 | os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1" 22 | 23 | # For Windows with limited GPU memory, use smaller chunks 24 | os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "max_split_size_mb:128" 25 | 26 | # Set ChromaDB path if provided via environment variables 27 | if "MCP_MEMORY_CHROMA_PATH" in os.environ: 28 | print(f"Using ChromaDB path: {os.environ['MCP_MEMORY_CHROMA_PATH']}", file=sys.stderr, flush=True) 29 | 30 | # Set backups path if provided via environment variables 31 | if "MCP_MEMORY_BACKUPS_PATH" in os.environ: 32 | print(f"Using backups path: {os.environ['MCP_MEMORY_BACKUPS_PATH']}", file=sys.stderr, flush=True) 33 | 34 | def print_info(text): 35 | """Print formatted info text.""" 36 | print(f"[INFO] {text}", file=sys.stderr, flush=True) 37 | 38 | def print_error(text): 39 | """Print formatted error text.""" 40 | print(f"[ERROR] {text}", file=sys.stderr, flush=True) 41 | 42 | def print_success(text): 43 | """Print formatted success text.""" 44 | print(f"[SUCCESS] {text}", file=sys.stderr, flush=True) 45 | 46 | def print_warning(text): 47 | """Print formatted warning text.""" 48 | print(f"[WARNING] {text}", file=sys.stderr, flush=True) 49 | 50 | def run_memory_server(): 51 | """Run the MCP Memory Service directly.""" 52 | print_info("Starting MCP Memory Service") 53 | 54 | # Save original sys.path and meta_path 55 | original_sys_path = sys.path.copy() 56 | original_meta_path = sys.meta_path 57 | 58 | # Temporarily disable import hooks 59 | sys.meta_path = [finder for finder in sys.meta_path 60 | if not hasattr(finder, 'find_spec') or 61 | not hasattr(finder, 'blocked_packages')] 62 | 63 | try: 64 | # Get the directory of this script 65 | script_dir = os.path.dirname(os.path.abspath(__file__)) 66 | parent_dir = os.path.dirname(script_dir) 67 | 68 | # Add src directory to path if it exists 69 | src_dir = os.path.join(parent_dir, "src") 70 | if os.path.exists(src_dir) and src_dir not in sys.path: 71 | print_info(f"Adding {src_dir} to sys.path") 72 | sys.path.insert(0, src_dir) 73 | 74 | # Add site-packages to sys.path 75 | site_packages = os.path.join(sys.prefix, 'Lib', 'site-packages') 76 | if site_packages not in sys.path: 77 | sys.path.insert(0, site_packages) 78 | 79 | # Try direct import from src directory 80 | server_path = os.path.join(src_dir, "mcp_memory_service", "server.py") 81 | if os.path.exists(server_path): 82 | print_info(f"Found server module at {server_path}") 83 | 84 | # Use importlib to load the module directly from the file 85 | module_name = "mcp_memory_service.server" 86 | spec = importlib.util.spec_from_file_location(module_name, server_path) 87 | if spec is None: 88 | print_error(f"Could not create spec from file: {server_path}") 89 | sys.exit(1) 90 | 91 | server = importlib.util.module_from_spec(spec) 92 | sys.modules[module_name] = server # Add to sys.modules to avoid import issues 93 | spec.loader.exec_module(server) 94 | 95 | print_success("Successfully imported mcp_memory_service.server from file") 96 | else: 97 | # Try to import using importlib 98 | print_info("Attempting to import mcp_memory_service.server using importlib") 99 | 100 | # First try to find the module in site-packages 101 | server_spec = importlib.machinery.PathFinder.find_spec('mcp_memory_service.server', [site_packages]) 102 | 103 | # If not found, try to find it in src directory 104 | if server_spec is None and os.path.exists(src_dir): 105 | server_spec = importlib.machinery.PathFinder.find_spec('mcp_memory_service.server', [src_dir]) 106 | 107 | if server_spec is None: 108 | print_error("Could not find mcp_memory_service.server module spec") 109 | sys.exit(1) 110 | 111 | # Load the server module 112 | server = importlib.util.module_from_spec(server_spec) 113 | server_spec.loader.exec_module(server) 114 | 115 | print_success("Successfully imported mcp_memory_service.server") 116 | 117 | # Run the memory server with error handling 118 | try: 119 | print_info("Calling mcp_memory_service.server.main()") 120 | server.main() 121 | except Exception as e: 122 | print_error(f"Error running memory server: {e}") 123 | traceback.print_exc(file=sys.stderr) 124 | sys.exit(1) 125 | except ImportError as e: 126 | print_error(f"Failed to import mcp_memory_service.server: {e}") 127 | traceback.print_exc(file=sys.stderr) 128 | sys.exit(1) 129 | except Exception as e: 130 | print_error(f"Error setting up memory server: {e}") 131 | traceback.print_exc(file=sys.stderr) 132 | sys.exit(1) 133 | finally: 134 | # Restore original sys.path and meta_path 135 | sys.path = original_sys_path 136 | sys.meta_path = original_meta_path 137 | 138 | if __name__ == "__main__": 139 | try: 140 | run_memory_server() 141 | except KeyboardInterrupt: 142 | print_info("Script interrupted by user") 143 | sys.exit(0) 144 | except Exception as e: 145 | print_error(f"Unhandled exception: {e}") 146 | traceback.print_exc(file=sys.stderr) 147 | sys.exit(1) -------------------------------------------------------------------------------- /scripts/run_migration.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Run Migration Script for MCP Memory Service 3 | 4 | echo Starting time-based recall functionality fix... 5 | 6 | REM Step 1: Create a backup 7 | echo Creating backup of memory database... 8 | python scripts\backup_memories.py 9 | if %ERRORLEVEL% NEQ 0 ( 10 | echo Warning: Backup operation returned error level %ERRORLEVEL% 11 | echo This might be a non-critical error. Continuing with migration... 12 | echo If migration fails, you can manually restore from existing backups if available. 13 | ) else ( 14 | echo Backup completed successfully. 15 | ) 16 | 17 | REM Step 2: Run the migration 18 | echo Running timestamp migration... 19 | python scripts\migrate_timestamps.py 20 | if %ERRORLEVEL% NEQ 0 ( 21 | echo Error during migration. Please check logs. 22 | exit /b 1 23 | ) 24 | echo Migration completed successfully. 25 | 26 | echo Time-based recall functionality fix completed. Please restart the MCP Memory Service. 27 | echo See scripts\TIME_BASED_RECALL_FIX.md for detailed information. -------------------------------------------------------------------------------- /scripts/run_migration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Make this script executable with: chmod +x scripts/run_migration.sh 3 | 4 | # Run Migration Script for MCP Memory Service 5 | echo "Starting time-based recall functionality fix..." 6 | 7 | # Step 1: Create a backup 8 | echo "Creating backup of memory database..." 9 | python scripts/backup_memories.py 10 | BACKUP_STATUS=$? 11 | if [ $BACKUP_STATUS -ne 0 ]; then 12 | echo "Warning: Backup operation returned status $BACKUP_STATUS" 13 | echo "This might be a non-critical error. Continuing with migration..." 14 | echo "If migration fails, you can manually restore from existing backups if available." 15 | else 16 | echo "Backup completed successfully." 17 | fi 18 | 19 | # Step 2: Run the migration 20 | echo "Running timestamp migration..." 21 | python scripts/migrate_timestamps.py 22 | if [ $? -ne 0 ]; then 23 | echo "Error during migration. Please check logs." 24 | exit 1 25 | fi 26 | echo "Migration completed successfully." 27 | 28 | echo "Time-based recall functionality fix completed. Please restart the MCP Memory Service." 29 | echo "See scripts/TIME_BASED_RECALL_FIX.md for detailed information." -------------------------------------------------------------------------------- /scripts/test-connection.py: -------------------------------------------------------------------------------- 1 | from chromadb import HttpClient 2 | 3 | def test_connection(port=8000): 4 | try: 5 | # Try to connect to local ChromaDB 6 | client = HttpClient(host='localhost', port=port) 7 | # Try a simple operation 8 | heartbeat = client.heartbeat() 9 | print(f"Successfully connected to ChromaDB on port {port}") 10 | print(f"Heartbeat: {heartbeat}") 11 | 12 | # List collections 13 | collections = client.list_collections() 14 | print("\nFound collections:") 15 | for collection in collections: 16 | print(f"- {collection.name} (count: {collection.count()})") 17 | 18 | except Exception as e: 19 | print(f"Error connecting to ChromaDB on port {port}: {str(e)}") 20 | 21 | if __name__ == "__main__": 22 | # Try default port 23 | test_connection() 24 | 25 | # If the above fails, you might want to try other common ports: 26 | # test_connection(8080) 27 | # test_connection(9000) 28 | -------------------------------------------------------------------------------- /scripts/validate_memories.py: -------------------------------------------------------------------------------- 1 | # scripts/validate_memories.py 2 | 3 | import asyncio 4 | import json 5 | import logging 6 | from mcp_memory_service.storage.chroma import ChromaMemoryStorage 7 | import argparse 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | async def validate_memory_data(storage): 12 | """Comprehensive validation of memory data with focus on tag formatting""" 13 | 14 | validation_results = { 15 | "total_memories": 0, 16 | "tag_format_issues": [], 17 | "missing_required_fields": [], 18 | "inconsistent_formats": [], 19 | "recommendations": [] 20 | } 21 | 22 | try: 23 | # Get all memories from the collection 24 | results = storage.collection.get( 25 | include=["metadatas", "documents"] 26 | ) 27 | 28 | validation_results["total_memories"] = len(results["ids"]) 29 | 30 | for i, meta in enumerate(results["metadatas"]): 31 | memory_id = results["ids"][i] 32 | 33 | # 1. Check Required Fields 34 | for field in ["content_hash", "tags"]: 35 | if field not in meta: 36 | validation_results["missing_required_fields"].append({ 37 | "memory_id": memory_id, 38 | "missing_field": field 39 | }) 40 | 41 | # 2. Validate Tag Format 42 | tags = meta.get("tags", "[]") 43 | try: 44 | if isinstance(tags, str): 45 | parsed_tags = json.loads(tags) 46 | if not isinstance(parsed_tags, list): 47 | validation_results["tag_format_issues"].append({ 48 | "memory_id": memory_id, 49 | "issue": "Tags not in list format after parsing", 50 | "current_format": type(parsed_tags).__name__ 51 | }) 52 | elif isinstance(tags, list): 53 | validation_results["tag_format_issues"].append({ 54 | "memory_id": memory_id, 55 | "issue": "Tags stored as raw list instead of JSON string", 56 | "current_format": "list" 57 | }) 58 | except json.JSONDecodeError: 59 | validation_results["tag_format_issues"].append({ 60 | "memory_id": memory_id, 61 | "issue": "Invalid JSON in tags field", 62 | "current_value": tags 63 | }) 64 | 65 | # 3. Check Tag Content 66 | try: 67 | stored_tags = json.loads(tags) if isinstance(tags, str) else tags 68 | if isinstance(stored_tags, list): 69 | for tag in stored_tags: 70 | if not isinstance(tag, str): 71 | validation_results["inconsistent_formats"].append({ 72 | "memory_id": memory_id, 73 | "issue": f"Non-string tag found: {type(tag).__name__}", 74 | "value": str(tag) 75 | }) 76 | except Exception as e: 77 | validation_results["inconsistent_formats"].append({ 78 | "memory_id": memory_id, 79 | "issue": f"Error processing tags: {str(e)}", 80 | "current_tags": tags 81 | }) 82 | 83 | # Generate Recommendations 84 | if validation_results["tag_format_issues"]: 85 | validation_results["recommendations"].append( 86 | "Run tag format migration to normalize all tags to JSON strings" 87 | ) 88 | if validation_results["missing_required_fields"]: 89 | validation_results["recommendations"].append( 90 | "Repair memories with missing required fields" 91 | ) 92 | if validation_results["inconsistent_formats"]: 93 | validation_results["recommendations"].append( 94 | "Clean up non-string tags in affected memories" 95 | ) 96 | 97 | return validation_results 98 | 99 | except Exception as e: 100 | logger.error(f"Error during validation: {str(e)}") 101 | validation_results["error"] = str(e) 102 | return validation_results 103 | 104 | async def run_validation_report(storage): 105 | """Generate a formatted validation report""" 106 | results = await validate_memory_data(storage) 107 | 108 | report = f""" 109 | Memory Data Validation Report 110 | ============================ 111 | Total Memories: {results['total_memories']} 112 | 113 | Issues Found: 114 | ------------- 115 | 1. Tag Format Issues: {len(results['tag_format_issues'])} 116 | 2. Missing Fields: {len(results['missing_required_fields'])} 117 | 3. Inconsistent Formats: {len(results['inconsistent_formats'])} 118 | 119 | Recommendations: 120 | --------------- 121 | {chr(10).join(f"- {r}" for r in results['recommendations'])} 122 | 123 | Detailed Issues: 124 | --------------- 125 | {json.dumps(results, indent=2)} 126 | """ 127 | 128 | return report 129 | 130 | async def main(): 131 | # Configure logging 132 | log_level = os.getenv('LOG_LEVEL', 'ERROR').upper() 133 | logging.basicConfig( 134 | level=getattr(logging, log_level, logging.ERROR), 135 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 136 | stream=sys.stderr 137 | ) 138 | 139 | # Initialize storage 140 | # storage = ChromaMemoryStorage("path/to/your/db") 141 | 142 | # Parse command line arguments 143 | parser = argparse.ArgumentParser(description='Validate memory data tags') 144 | parser.add_argument('--db-path', required=True, help='Path to ChromaDB database') 145 | args = parser.parse_args() 146 | 147 | # Initialize storage with provided path 148 | logger.info(f"Connecting to database at: {args.db_path}") 149 | storage = ChromaMemoryStorage(args.db_path) 150 | 151 | # Run validation and get report 152 | report = await run_validation_report(storage) 153 | 154 | # Print report to console 155 | print(report) 156 | 157 | # Save report to file 158 | with open('validation_report.txt', 'w') as f: 159 | f.write(report) 160 | 161 | if __name__ == "__main__": 162 | asyncio.run(main()) -------------------------------------------------------------------------------- /scripts/verify_pytorch_windows.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Verification script for PyTorch installation on Windows. 4 | This script checks if PyTorch is properly installed and configured for Windows. 5 | """ 6 | import os 7 | import sys 8 | import platform 9 | import subprocess 10 | import importlib.util 11 | 12 | def print_header(text): 13 | """Print a formatted header.""" 14 | print("\n" + "=" * 80) 15 | print(f" {text}") 16 | print("=" * 80) 17 | 18 | def print_info(text): 19 | """Print formatted info text.""" 20 | print(f" → {text}") 21 | 22 | def print_success(text): 23 | """Print formatted success text.""" 24 | print(f" ✅ {text}") 25 | 26 | def print_error(text): 27 | """Print formatted error text.""" 28 | print(f" ❌ ERROR: {text}") 29 | 30 | def print_warning(text): 31 | """Print formatted warning text.""" 32 | print(f" ⚠️ {text}") 33 | 34 | def check_system(): 35 | """Check if running on Windows.""" 36 | system = platform.system().lower() 37 | if system != "windows": 38 | print_warning(f"This script is designed for Windows, but you're running on {system.capitalize()}") 39 | else: 40 | print_info(f"Running on {platform.system()} {platform.release()}") 41 | 42 | print_info(f"Python version: {platform.python_version()}") 43 | print_info(f"Architecture: {platform.machine()}") 44 | 45 | return system == "windows" 46 | 47 | def check_pytorch_installation(): 48 | """Check if PyTorch is installed and properly configured.""" 49 | try: 50 | import torch 51 | print_success(f"PyTorch is installed (version {torch.__version__})") 52 | 53 | # Check if PyTorch was installed from the correct index URL 54 | if hasattr(torch, '_C'): 55 | print_success("PyTorch C extensions are available") 56 | else: 57 | print_warning("PyTorch C extensions might not be properly installed") 58 | 59 | # Check CUDA availability 60 | if torch.cuda.is_available(): 61 | print_success(f"CUDA is available (version {torch.version.cuda})") 62 | print_info(f"GPU: {torch.cuda.get_device_name(0)}") 63 | print_info(f"GPU Memory: {torch.cuda.get_device_properties(0).total_memory / (1024**3):.2f} GB") 64 | else: 65 | print_info("CUDA is not available, using CPU only") 66 | 67 | # Check if DirectML is available 68 | try: 69 | import torch_directml 70 | print_success(f"DirectML is available (version {torch_directml.__version__})") 71 | 72 | # Check for Intel ARC GPU 73 | try: 74 | ps_cmd = "Get-WmiObject Win32_VideoController | Select-Object Name | Format-List" 75 | gpu_output = subprocess.check_output(['powershell', '-Command', ps_cmd], 76 | stderr=subprocess.DEVNULL, 77 | universal_newlines=True) 78 | 79 | if 'Intel(R) Arc(TM)' in gpu_output or 'Intel ARC' in gpu_output: 80 | print_success("Intel ARC GPU detected, DirectML support is available") 81 | elif 'Intel' in gpu_output: 82 | print_success("Intel GPU detected, DirectML support is available") 83 | elif 'AMD' in gpu_output or 'Radeon' in gpu_output: 84 | print_success("AMD GPU detected, DirectML support is available") 85 | except (subprocess.SubprocessError, FileNotFoundError): 86 | pass 87 | 88 | # Test a simple DirectML tensor operation 89 | try: 90 | dml = torch_directml.device() 91 | x_dml = torch.rand(5, 3, device=dml) 92 | y_dml = torch.rand(5, 3, device=dml) 93 | z_dml = x_dml + y_dml 94 | print_success("DirectML tensor operations work correctly") 95 | except Exception as e: 96 | print_warning(f"DirectML tensor operations failed: {e}") 97 | except ImportError: 98 | print_info("DirectML is not available") 99 | 100 | # Check for Intel/AMD GPUs that could benefit from DirectML 101 | try: 102 | ps_cmd = "Get-WmiObject Win32_VideoController | Select-Object Name | Format-List" 103 | gpu_output = subprocess.check_output(['powershell', '-Command', ps_cmd], 104 | stderr=subprocess.DEVNULL, 105 | universal_newlines=True) 106 | 107 | if 'Intel(R) Arc(TM)' in gpu_output or 'Intel ARC' in gpu_output: 108 | print_warning("Intel ARC GPU detected, but DirectML is not installed") 109 | print_info("Consider installing torch-directml for better performance") 110 | elif 'Intel' in gpu_output or 'AMD' in gpu_output or 'Radeon' in gpu_output: 111 | print_warning("Intel/AMD GPU detected, but DirectML is not installed") 112 | print_info("Consider installing torch-directml for better performance") 113 | except (subprocess.SubprocessError, FileNotFoundError): 114 | pass 115 | 116 | # Test a simple tensor operation 117 | try: 118 | x = torch.rand(5, 3) 119 | y = torch.rand(5, 3) 120 | z = x + y 121 | print_success("Basic tensor operations work correctly") 122 | except Exception as e: 123 | print_error(f"Failed to perform basic tensor operations: {e}") 124 | return False 125 | 126 | return True 127 | except ImportError: 128 | print_error("PyTorch is not installed") 129 | return False 130 | except Exception as e: 131 | print_error(f"Error checking PyTorch installation: {e}") 132 | return False 133 | 134 | def suggest_installation(): 135 | """Suggest PyTorch installation commands.""" 136 | print_header("Installation Suggestions") 137 | print_info("To install PyTorch for Windows, use one of the following commands:") 138 | print_info("\nFor CUDA support (NVIDIA GPUs):") 139 | print("pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118") 140 | 141 | print_info("\nFor CPU-only:") 142 | print("pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu") 143 | 144 | print_info("\nFor DirectML support (AMD/Intel GPUs):") 145 | print("pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu") 146 | print("pip install torch-directml>=0.2.0") 147 | 148 | print_info("\nFor Intel ARC Pro Graphics:") 149 | print("pip install torch==2.2.0 torchvision==2.2.0 torchaudio==2.2.0 --index-url https://download.pytorch.org/whl/cpu") 150 | print("pip install torch-directml>=0.2.0") 151 | 152 | print_info("\nFor dual GPU setups (NVIDIA + Intel):") 153 | print("pip install torch==2.2.0 torchvision==2.2.0 torchaudio==2.2.0 --index-url https://download.pytorch.org/whl/cu118") 154 | print("pip install torch-directml>=0.2.0") 155 | 156 | print_info("\nAfter installing PyTorch, run this script again to verify the installation.") 157 | 158 | def main(): 159 | """Main function.""" 160 | print_header("PyTorch Windows Installation Verification") 161 | 162 | is_windows = check_system() 163 | if not is_windows: 164 | print_warning("This script is designed for Windows, but may still provide useful information") 165 | 166 | pytorch_installed = check_pytorch_installation() 167 | 168 | if not pytorch_installed: 169 | suggest_installation() 170 | return 1 171 | 172 | print_header("Verification Complete") 173 | print_success("PyTorch is properly installed and configured for Windows") 174 | return 0 175 | 176 | if __name__ == "__main__": 177 | sys.exit(main()) -------------------------------------------------------------------------------- /scripts/verify_torch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Verify PyTorch installation and functionality. 4 | This script attempts to import PyTorch and run basic operations. 5 | """ 6 | import os 7 | import sys 8 | 9 | # Disable sitecustomize.py and other import hooks to prevent recursion issues 10 | os.environ["PYTHONNOUSERSITE"] = "1" # Disable user site-packages 11 | os.environ["PYTHONPATH"] = "" # Clear PYTHONPATH 12 | 13 | def print_info(text): 14 | """Print formatted info text.""" 15 | print(f"[INFO] {text}") 16 | 17 | def print_error(text): 18 | """Print formatted error text.""" 19 | print(f"[ERROR] {text}") 20 | 21 | def print_success(text): 22 | """Print formatted success text.""" 23 | print(f"[SUCCESS] {text}") 24 | 25 | def print_warning(text): 26 | """Print formatted warning text.""" 27 | print(f"[WARNING] {text}") 28 | 29 | def verify_torch(): 30 | """Verify PyTorch installation and functionality.""" 31 | print_info("Verifying PyTorch installation") 32 | 33 | # Add site-packages to sys.path 34 | site_packages = os.path.join(sys.prefix, 'Lib', 'site-packages') 35 | if site_packages not in sys.path: 36 | sys.path.insert(0, site_packages) 37 | 38 | # Print sys.path for debugging 39 | print_info("Python path:") 40 | for path in sys.path: 41 | print(f" - {path}") 42 | 43 | # Try to import torch 44 | try: 45 | print_info("Attempting to import torch") 46 | import torch 47 | print_success(f"PyTorch is installed (version {torch.__version__})") 48 | print_info(f"PyTorch location: {torch.__file__}") 49 | 50 | # Check if CUDA is available 51 | if torch.cuda.is_available(): 52 | print_success(f"CUDA is available (version {torch.version.cuda})") 53 | print_info(f"GPU: {torch.cuda.get_device_name(0)}") 54 | 55 | # Test a simple CUDA operation 56 | try: 57 | x = torch.rand(5, 3).cuda() 58 | y = torch.rand(5, 3).cuda() 59 | z = x + y 60 | print_success("Basic CUDA tensor operations work correctly") 61 | except Exception as e: 62 | print_warning(f"CUDA tensor operations failed: {e}") 63 | print_warning("Falling back to CPU mode") 64 | else: 65 | print_info("CUDA is not available, using CPU-only mode") 66 | 67 | # Test a simple tensor operation 68 | try: 69 | x = torch.rand(5, 3) 70 | y = torch.rand(5, 3) 71 | z = x + y 72 | print_success("Basic tensor operations work correctly") 73 | except Exception as e: 74 | print_error(f"Failed to perform basic tensor operations: {e}") 75 | return False 76 | 77 | return True 78 | except ImportError as e: 79 | print_error(f"PyTorch is not installed: {e}") 80 | return False 81 | except Exception as e: 82 | print_error(f"Error checking PyTorch installation: {e}") 83 | import traceback 84 | traceback.print_exc() 85 | return False 86 | 87 | def main(): 88 | """Main function.""" 89 | if verify_torch(): 90 | print_success("PyTorch verification completed successfully") 91 | else: 92 | print_error("PyTorch verification failed") 93 | sys.exit(1) 94 | 95 | if __name__ == "__main__": 96 | main() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Setup script for MCP Memory Service with cross-platform compatibility. 4 | This script detects the system architecture and installs the appropriate dependencies. 5 | """ 6 | import os 7 | import sys 8 | import platform 9 | import subprocess 10 | from setuptools import setup, find_packages 11 | 12 | # Determine the system architecture and platform 13 | SYSTEM = platform.system().lower() 14 | MACHINE = platform.machine().lower() 15 | IS_WINDOWS = SYSTEM == "windows" 16 | IS_MACOS = SYSTEM == "darwin" 17 | IS_LINUX = SYSTEM == "linux" 18 | IS_ARM = MACHINE in ("arm64", "aarch64") 19 | IS_X86 = MACHINE in ("x86_64", "amd64", "x64") 20 | PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}" 21 | 22 | # Check for CUDA availability 23 | def has_cuda(): 24 | """Check if CUDA is available on the system.""" 25 | if IS_WINDOWS: 26 | return os.path.exists(os.environ.get('CUDA_PATH', 'C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA')) 27 | elif IS_LINUX: 28 | return os.path.exists('/usr/local/cuda') or 'CUDA_HOME' in os.environ 29 | return False 30 | 31 | # Check for ROCm availability 32 | def has_rocm(): 33 | """Check if ROCm is available on the system.""" 34 | if not IS_LINUX: 35 | return False 36 | return os.path.exists('/opt/rocm') or 'ROCM_HOME' in os.environ 37 | 38 | # Check for MPS availability (Apple Silicon) 39 | def has_mps(): 40 | """Check if MPS (Metal Performance Shaders) is available on Apple Silicon.""" 41 | if not (IS_MACOS and IS_ARM): 42 | return False 43 | try: 44 | # Check if Metal is supported 45 | result = subprocess.run( 46 | ['system_profiler', 'SPDisplaysDataType'], 47 | capture_output=True, 48 | text=True 49 | ) 50 | return 'Metal' in result.stdout 51 | except (subprocess.SubprocessError, FileNotFoundError): 52 | return False 53 | 54 | # Determine PyTorch package based on platform 55 | def get_torch_packages(): 56 | """Get the appropriate PyTorch packages for the current platform.""" 57 | packages = [] 58 | 59 | # For Apple Silicon with MPS 60 | if IS_MACOS and IS_ARM and has_mps(): 61 | print("Detected Apple Silicon with MPS support") 62 | packages.extend([ 63 | "torch>=2.0.0", 64 | "torchvision>=0.15.0", 65 | "torchaudio>=2.0.0" 66 | ]) 67 | # For macOS Intel 68 | elif IS_MACOS and IS_X86: 69 | print("Detected macOS with Intel CPU") 70 | packages.extend([ 71 | "torch==2.0.1", # Fixed version for Intel Macs 72 | "torchvision==0.15.2", 73 | "torchaudio==2.0.2", 74 | "sentence-transformers==2.2.2" # Compatible version with torch 2.0.1 75 | ]) 76 | # For CUDA-enabled systems 77 | elif has_cuda(): 78 | print("Detected CUDA support") 79 | # Note: Windows with CUDA will be handled separately in install_requires 80 | if not IS_WINDOWS: 81 | # Linux with CUDA 82 | packages.extend([ 83 | "torch>=2.0.0", 84 | "torchvision>=0.15.0", 85 | "torchaudio>=2.0.0" 86 | ]) 87 | # For ROCm-enabled systems (Linux only) 88 | elif IS_LINUX and has_rocm(): 89 | print("Detected ROCm support") 90 | packages.extend([ 91 | "torch>=2.0.0", 92 | "torchvision>=0.15.0", 93 | "torchaudio>=2.0.0" 94 | ]) 95 | # For Windows with DirectML 96 | elif IS_WINDOWS: 97 | try: 98 | # Check if DirectML is available 99 | import pkg_resources 100 | pkg_resources.get_distribution('torch-directml') 101 | print("Detected DirectML support") 102 | # Note: Base PyTorch for Windows will be handled separately 103 | packages.append("torch-directml>=0.2.0") 104 | except (ImportError, pkg_resources.DistributionNotFound): 105 | # Windows platforms need special handling 106 | print("Windows platform detected - PyTorch will be installed separately") 107 | # Don't add PyTorch packages here, they'll be installed separately 108 | # Default to CPU-only for all other platforms 109 | else: 110 | print("Using CPU-only PyTorch") 111 | packages.extend([ 112 | "torch>=2.0.0", 113 | "torchvision>=0.15.0", 114 | "torchaudio>=2.0.0" 115 | ]) 116 | 117 | return packages 118 | 119 | # Get additional platform-specific packages 120 | def get_platform_specific_packages(): 121 | """Get additional platform-specific packages.""" 122 | packages = [] 123 | 124 | # For systems with limited resources, use ONNX Runtime 125 | if os.environ.get('MCP_MEMORY_USE_ONNX', '').lower() in ('1', 'true', 'yes'): 126 | packages.append("onnxruntime>=1.15.0") 127 | 128 | # For Windows with DirectML but without torch-directml already installed 129 | if IS_WINDOWS and not any('torch-directml' in pkg for pkg in packages): 130 | try: 131 | import pkg_resources 132 | pkg_resources.get_distribution('torch-directml') 133 | except (ImportError, pkg_resources.DistributionNotFound): 134 | if os.environ.get('MCP_MEMORY_USE_DIRECTML', '').lower() in ('1', 'true', 'yes'): 135 | packages.append("torch-directml>=0.2.0") 136 | 137 | return packages 138 | 139 | # Read requirements from requirements.txt 140 | with open('requirements.txt', 'r') as f: 141 | requirements = [line.strip() for line in f if line.strip() and not line.startswith('#') and not line.startswith('torch')] 142 | 143 | # Add platform-specific PyTorch packages 144 | requirements.extend(get_torch_packages()) 145 | 146 | # Add other platform-specific packages 147 | requirements.extend(get_platform_specific_packages()) 148 | 149 | # Print detected environment and packages 150 | print(f"System: {SYSTEM} {MACHINE}") 151 | print(f"Python: {PYTHON_VERSION}") 152 | print("Installing packages:") 153 | for req in requirements: 154 | print(f" - {req}") 155 | 156 | setup( 157 | name="mcp-memory-service", 158 | version="0.1.0", 159 | description="A semantic memory service using ChromaDB and sentence-transformers", 160 | author="Heinrich Krupp", 161 | author_email="heinrich.krupp@gmail.com", 162 | packages=find_packages(where="src"), 163 | package_dir={"": "src"}, 164 | python_requires=">=3.10", 165 | install_requires=requirements, 166 | entry_points={ 167 | "console_scripts": [ 168 | "memory=mcp_memory_service.server:main", 169 | ], 170 | }, 171 | classifiers=[ 172 | "Development Status :: 4 - Beta", 173 | "Intended Audience :: Developers", 174 | "License :: OSI Approved :: MIT License", 175 | "Programming Language :: Python :: 3", 176 | "Programming Language :: Python :: 3.10", 177 | "Programming Language :: Python :: 3.11", 178 | ], 179 | ) -------------------------------------------------------------------------------- /smithery.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 | - chromaDbPath 10 | - backupsPath 11 | properties: 12 | chromaDbPath: 13 | type: string 14 | description: Path to ChromaDB storage. 15 | backupsPath: 16 | type: string 17 | description: Path for backups. 18 | commandFunction: 19 | # A function that produces the CLI command to start the MCP on stdio. 20 | |- 21 | (config) => ({ 22 | command: 'python', 23 | args: ['-u', 'src/mcp_memory_service/server.py'], 24 | env: { 25 | MCP_MEMORY_CHROMA_PATH: config.chromaDbPath, 26 | MCP_MEMORY_BACKUPS_PATH: config.backupsPath, 27 | PYTHONUNBUFFERED: '1' 28 | } 29 | }) -------------------------------------------------------------------------------- /src/mcp_memory_service/__init__.py: -------------------------------------------------------------------------------- 1 | """MCP Memory Service initialization.""" 2 | 3 | __version__ = "0.1.0" 4 | 5 | from .models import Memory, MemoryQueryResult 6 | from .storage import MemoryStorage, ChromaMemoryStorage 7 | from .utils import generate_content_hash 8 | 9 | __all__ = [ 10 | 'Memory', 11 | 'MemoryQueryResult', 12 | 'MemoryStorage', 13 | 'ChromaMemoryStorage', 14 | 'generate_content_hash' 15 | ] 16 | 17 | -------------------------------------------------------------------------------- /src/mcp_memory_service/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | MCP Memory Service 3 | Copyright (c) 2024 Heinrich Krupp 4 | Licensed under the MIT License. See LICENSE file in the project root for full license text. 5 | """ 6 | import os 7 | import sys 8 | from pathlib import Path 9 | import time 10 | import logging 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | def validate_and_create_path(path: str) -> str: 15 | """Validate and create a directory path, ensuring it's writable. 16 | 17 | This function ensures that the specified directory path exists and is writable. 18 | It performs several checks and has a retry mechanism to handle potential race 19 | conditions, especially when running in environments like Claude Desktop where 20 | file system operations might be more restricted. 21 | """ 22 | try: 23 | # Convert to absolute path and expand user directory if present (e.g. ~) 24 | abs_path = os.path.abspath(os.path.expanduser(path)) 25 | logger.debug(f"Validating path: {abs_path}") 26 | 27 | # Create directory and all parents if they don't exist 28 | try: 29 | os.makedirs(abs_path, exist_ok=True) 30 | logger.debug(f"Created directory (or already exists): {abs_path}") 31 | except Exception as e: 32 | logger.error(f"Error creating directory {abs_path}: {str(e)}") 33 | raise PermissionError(f"Cannot create directory {abs_path}: {str(e)}") 34 | 35 | # Add small delay to prevent potential race conditions on macOS during initial write test 36 | time.sleep(0.1) 37 | 38 | # Verify that the path exists and is a directory 39 | if not os.path.exists(abs_path): 40 | logger.error(f"Path does not exist after creation attempt: {abs_path}") 41 | raise PermissionError(f"Path does not exist: {abs_path}") 42 | 43 | if not os.path.isdir(abs_path): 44 | logger.error(f"Path is not a directory: {abs_path}") 45 | raise PermissionError(f"Path is not a directory: {abs_path}") 46 | 47 | # Write test with retry mechanism 48 | max_retries = 3 49 | retry_delay = 0.5 50 | test_file = os.path.join(abs_path, '.write_test') 51 | 52 | for attempt in range(max_retries): 53 | try: 54 | logger.debug(f"Testing write permissions (attempt {attempt+1}/{max_retries}): {test_file}") 55 | with open(test_file, 'w') as f: 56 | f.write('test') 57 | 58 | if os.path.exists(test_file): 59 | logger.debug(f"Successfully wrote test file: {test_file}") 60 | os.remove(test_file) 61 | logger.debug(f"Successfully removed test file: {test_file}") 62 | logger.info(f"Directory {abs_path} is writable.") 63 | return abs_path 64 | else: 65 | logger.warning(f"Test file was not created: {test_file}") 66 | except Exception as e: 67 | logger.warning(f"Error during write test (attempt {attempt+1}/{max_retries}): {str(e)}") 68 | if attempt < max_retries - 1: 69 | logger.debug(f"Retrying after {retry_delay}s...") 70 | time.sleep(retry_delay) 71 | else: 72 | logger.error(f"All write test attempts failed for {abs_path}") 73 | raise PermissionError(f"Directory {abs_path} is not writable: {str(e)}") 74 | 75 | return abs_path 76 | except Exception as e: 77 | logger.error(f"Error validating path {path}: {str(e)}") 78 | raise 79 | 80 | # Determine base directory - prefer local over Cloud 81 | def get_base_directory() -> str: 82 | """Get base directory for storage, with fallback options.""" 83 | # First choice: Environment variable 84 | if base_dir := os.getenv('MCP_MEMORY_BASE_DIR'): 85 | return validate_and_create_path(base_dir) 86 | 87 | # Second choice: Local app data directory 88 | home = str(Path.home()) 89 | if sys.platform == 'darwin': # macOS 90 | base = os.path.join(home, 'Library', 'Application Support', 'mcp-memory') 91 | elif sys.platform == 'win32': # Windows 92 | base = os.path.join(os.getenv('LOCALAPPDATA', ''), 'mcp-memory') 93 | else: # Linux and others 94 | base = os.path.join(home, '.local', 'share', 'mcp-memory') 95 | 96 | return validate_and_create_path(base) 97 | 98 | # Initialize paths 99 | try: 100 | BASE_DIR = get_base_directory() 101 | 102 | # Try multiple environment variable names for ChromaDB path 103 | chroma_path = None 104 | for env_var in ['MCP_MEMORY_CHROMA_PATH', 'mcpMemoryChromaPath']: 105 | if path := os.getenv(env_var): 106 | chroma_path = path 107 | logger.info(f"Using {env_var}={path} for ChromaDB path") 108 | break 109 | 110 | # If no environment variable is set, use the default path 111 | if not chroma_path: 112 | chroma_path = os.path.join(BASE_DIR, 'chroma_db') 113 | logger.info(f"No ChromaDB path environment variable found, using default: {chroma_path}") 114 | 115 | # Try multiple environment variable names for backups path 116 | backups_path = None 117 | for env_var in ['MCP_MEMORY_BACKUPS_PATH', 'mcpMemoryBackupsPath']: 118 | if path := os.getenv(env_var): 119 | backups_path = path 120 | logger.info(f"Using {env_var}={path} for backups path") 121 | break 122 | 123 | # If no environment variable is set, use the default path 124 | if not backups_path: 125 | backups_path = os.path.join(BASE_DIR, 'backups') 126 | logger.info(f"No backups path environment variable found, using default: {backups_path}") 127 | 128 | CHROMA_PATH = validate_and_create_path(chroma_path) 129 | BACKUPS_PATH = validate_and_create_path(backups_path) 130 | 131 | # Print the final paths used 132 | logger.info(f"Using ChromaDB path: {CHROMA_PATH}") 133 | logger.info(f"Using backups path: {BACKUPS_PATH}") 134 | 135 | except Exception as e: 136 | logger.error(f"Fatal error initializing paths: {str(e)}") 137 | sys.exit(1) 138 | 139 | # Server settings 140 | SERVER_NAME = "memory" 141 | SERVER_VERSION = "0.2.0" 142 | 143 | # ChromaDB settings 144 | CHROMA_SETTINGS = { 145 | "anonymized_telemetry": False, 146 | "allow_reset": True, 147 | "is_persistent": True, 148 | "chroma_db_impl": "duckdb+parquet" 149 | } 150 | 151 | # Collection settings 152 | COLLECTION_METADATA = { 153 | "hnsw:space": "cosine", 154 | "hnsw:construction_ef": 100, # Increased for better accuracy 155 | "hnsw:search_ef": 100 # Increased for better search results 156 | } 157 | -------------------------------------------------------------------------------- /src/mcp_memory_service/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .memory import Memory, MemoryQueryResult 2 | 3 | __all__ = ['Memory', 'MemoryQueryResult'] -------------------------------------------------------------------------------- /src/mcp_memory_service/models/memory.py: -------------------------------------------------------------------------------- 1 | """Memory-related data models.""" 2 | from dataclasses import dataclass, field 3 | from typing import List, Optional, Dict, Any 4 | from datetime import datetime 5 | import time 6 | from dateutil import parser as dateutil_parser # For robust ISO parsing 7 | 8 | @dataclass 9 | class Memory: 10 | """Represents a single memory entry.""" 11 | content: str 12 | content_hash: str 13 | tags: List[str] = field(default_factory=list) 14 | memory_type: Optional[str] = None 15 | metadata: Dict[str, Any] = field(default_factory=dict) 16 | embedding: Optional[List[float]] = None 17 | 18 | # Timestamp fields with flexible input formats 19 | # Store as float and ISO8601 string for maximum compatibility 20 | created_at: Optional[float] = None 21 | created_at_iso: Optional[str] = None 22 | updated_at: Optional[float] = None 23 | updated_at_iso: Optional[str] = None 24 | 25 | # Legacy timestamp field (maintain for backward compatibility) 26 | timestamp: datetime = field(default_factory=datetime.now) 27 | 28 | def __post_init__(self): 29 | """Initialize timestamps after object creation.""" 30 | # Synchronize the timestamps 31 | self._sync_timestamps( 32 | created_at=self.created_at, 33 | created_at_iso=self.created_at_iso, 34 | updated_at=self.updated_at, 35 | updated_at_iso=self.updated_at_iso 36 | ) 37 | 38 | def _sync_timestamps(self, created_at=None, created_at_iso=None, updated_at=None, updated_at_iso=None): 39 | """ 40 | Synchronize timestamp fields to ensure all formats are available. 41 | Handles any combination of inputs and fills in missing values. 42 | Always uses UTC time. 43 | """ 44 | now = time.time() 45 | 46 | def iso_to_float(iso_str: str) -> float: 47 | """Convert ISO string to float timestamp.""" 48 | return dateutil_parser.isoparse(iso_str).timestamp() 49 | 50 | def float_to_iso(ts: float) -> str: 51 | """Convert float timestamp to ISO string.""" 52 | return datetime.utcfromtimestamp(ts).isoformat() + "Z" 53 | 54 | # Handle created_at 55 | if created_at is not None and created_at_iso is not None: 56 | # Validate that they represent the same time 57 | try: 58 | iso_ts = iso_to_float(created_at_iso) 59 | if abs(created_at - iso_ts) > 1e-6: # Allow for small floating-point differences 60 | raise ValueError("created_at and created_at_iso do not match") 61 | self.created_at = created_at 62 | self.created_at_iso = created_at_iso 63 | except ValueError as e: 64 | logger.warning(f"Invalid created_at or created_at_iso: {e}") 65 | self.created_at = now 66 | self.created_at_iso = float_to_iso(now) 67 | elif created_at is not None: 68 | self.created_at = created_at 69 | self.created_at_iso = float_to_iso(created_at) 70 | elif created_at_iso: 71 | try: 72 | self.created_at = iso_to_float(created_at_iso) 73 | self.created_at_iso = created_at_iso 74 | except ValueError as e: 75 | logger.warning(f"Invalid created_at_iso: {e}") 76 | self.created_at = now 77 | self.created_at_iso = float_to_iso(now) 78 | else: 79 | self.created_at = now 80 | self.created_at_iso = float_to_iso(now) 81 | 82 | # Handle updated_at 83 | if updated_at is not None and updated_at_iso is not None: 84 | # Validate that they represent the same time 85 | try: 86 | iso_ts = iso_to_float(updated_at_iso) 87 | if abs(updated_at - iso_ts) > 1e-6: # Allow for small floating-point differences 88 | raise ValueError("updated_at and updated_at_iso do not match") 89 | self.updated_at = updated_at 90 | self.updated_at_iso = updated_at_iso 91 | except ValueError as e: 92 | logger.warning(f"Invalid updated_at or updated_at_iso: {e}") 93 | self.updated_at = now 94 | self.updated_at_iso = float_to_iso(now) 95 | elif updated_at is not None: 96 | self.updated_at = updated_at 97 | self.updated_at_iso = float_to_iso(updated_at) 98 | elif updated_at_iso: 99 | try: 100 | self.updated_at = iso_to_float(updated_at_iso) 101 | self.updated_at_iso = updated_at_iso 102 | except ValueError as e: 103 | logger.warning(f"Invalid updated_at_iso: {e}") 104 | self.updated_at = now 105 | self.updated_at_iso = float_to_iso(now) 106 | else: 107 | self.updated_at = now 108 | self.updated_at_iso = float_to_iso(now) 109 | 110 | # Update legacy timestamp field for backward compatibility 111 | self.timestamp = datetime.utcfromtimestamp(self.created_at) 112 | 113 | def touch(self): 114 | """Update the updated_at timestamps to the current time.""" 115 | now = time.time() 116 | self.updated_at = now 117 | self.updated_at_iso = datetime.utcfromtimestamp(now).isoformat() + "Z" 118 | 119 | def to_dict(self) -> Dict[str, Any]: 120 | """Convert memory to dictionary format for storage.""" 121 | # Ensure timestamps are synchronized 122 | self._sync_timestamps( 123 | created_at=self.created_at, 124 | created_at_iso=self.created_at_iso, 125 | updated_at=self.updated_at, 126 | updated_at_iso=self.updated_at_iso 127 | ) 128 | 129 | return { 130 | "content": self.content, 131 | "content_hash": self.content_hash, 132 | "tags_str": ",".join(self.tags) if self.tags else "", 133 | "type": self.memory_type, 134 | # Store timestamps in all formats for better compatibility 135 | "timestamp": int(self.created_at), # Legacy timestamp (int) 136 | "timestamp_float": self.created_at, # Legacy timestamp (float) 137 | "timestamp_str": self.created_at_iso, # Legacy timestamp (ISO) 138 | # New timestamp fields 139 | "created_at": self.created_at, 140 | "created_at_iso": self.created_at_iso, 141 | "updated_at": self.updated_at, 142 | "updated_at_iso": self.updated_at_iso, 143 | **self.metadata 144 | } 145 | 146 | @classmethod 147 | def from_dict(cls, data: Dict[str, Any], embedding: Optional[List[float]] = None) -> 'Memory': 148 | """Create a Memory instance from dictionary data.""" 149 | tags = data.get("tags_str", "").split(",") if data.get("tags_str") else [] 150 | 151 | # Extract timestamps with different priorities 152 | # First check new timestamp fields (created_at/updated_at) 153 | created_at = data.get("created_at") 154 | created_at_iso = data.get("created_at_iso") 155 | updated_at = data.get("updated_at") 156 | updated_at_iso = data.get("updated_at_iso") 157 | 158 | # If new fields are missing, try to get from legacy timestamp fields 159 | if created_at is None and created_at_iso is None: 160 | if "timestamp_float" in data: 161 | created_at = float(data["timestamp_float"]) 162 | elif "timestamp" in data: 163 | created_at = float(data["timestamp"]) 164 | 165 | if "timestamp_str" in data and created_at_iso is None: 166 | created_at_iso = data["timestamp_str"] 167 | 168 | # Create metadata dictionary without special fields 169 | metadata = { 170 | k: v for k, v in data.items() 171 | if k not in [ 172 | "content", "content_hash", "tags_str", "type", 173 | "timestamp", "timestamp_float", "timestamp_str", 174 | "created_at", "created_at_iso", "updated_at", "updated_at_iso" 175 | ] 176 | } 177 | 178 | # Create memory instance with synchronized timestamps 179 | return cls( 180 | content=data["content"], 181 | content_hash=data["content_hash"], 182 | tags=[tag for tag in tags if tag], # Filter out empty tags 183 | memory_type=data.get("type"), 184 | metadata=metadata, 185 | embedding=embedding, 186 | created_at=created_at, 187 | created_at_iso=created_at_iso, 188 | updated_at=updated_at, 189 | updated_at_iso=updated_at_iso 190 | ) 191 | 192 | @dataclass 193 | class MemoryQueryResult: 194 | """Represents a memory query result with relevance score and debug information.""" 195 | memory: Memory 196 | relevance_score: float 197 | debug_info: Dict[str, Any] = field(default_factory=dict) 198 | -------------------------------------------------------------------------------- /src/mcp_memory_service/storage/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import MemoryStorage 2 | from .chroma import ChromaMemoryStorage 3 | 4 | __all__ = ['MemoryStorage', 'ChromaMemoryStorage'] -------------------------------------------------------------------------------- /src/mcp_memory_service/storage/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | MCP Memory Service 3 | Copyright (c) 2024 Heinrich Krupp 4 | Licensed under the MIT License. See LICENSE file in the project root for full license text. 5 | """ 6 | from abc import ABC, abstractmethod 7 | from typing import List, Optional, Dict, Any, Tuple 8 | from ..models.memory import Memory, MemoryQueryResult 9 | 10 | class MemoryStorage(ABC): 11 | """Abstract base class for memory storage implementations.""" 12 | 13 | @abstractmethod 14 | async def store(self, memory: Memory) -> Tuple[bool, str]: 15 | """Store a memory. Returns (success, message).""" 16 | pass 17 | 18 | @abstractmethod 19 | async def retrieve(self, query: str, n_results: int = 5) -> List[MemoryQueryResult]: 20 | """Retrieve memories by semantic search.""" 21 | pass 22 | 23 | @abstractmethod 24 | async def search_by_tag(self, tags: List[str]) -> List[Memory]: 25 | """Search memories by tags.""" 26 | pass 27 | 28 | @abstractmethod 29 | async def delete(self, content_hash: str) -> Tuple[bool, str]: 30 | """Delete a memory by its hash.""" 31 | pass 32 | 33 | @abstractmethod 34 | async def delete_by_tag(self, tag: str) -> Tuple[int, str]: 35 | """Delete memories by tag. Returns (count_deleted, message).""" 36 | pass 37 | 38 | @abstractmethod 39 | async def cleanup_duplicates(self) -> Tuple[int, str]: 40 | """Remove duplicate memories. Returns (count_removed, message).""" 41 | pass -------------------------------------------------------------------------------- /src/mcp_memory_service/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .hashing import generate_content_hash 2 | 3 | __all__ = ['generate_content_hash'] -------------------------------------------------------------------------------- /src/mcp_memory_service/utils/db_utils.py: -------------------------------------------------------------------------------- 1 | """Utilities for database validation and health checks.""" 2 | from typing import Dict, Any, Tuple 3 | import logging 4 | import os 5 | import json 6 | from datetime import datetime 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | async def validate_database(storage) -> Tuple[bool, str]: 11 | """Validate database health and configuration.""" 12 | try: 13 | # Check if collection exists and is accessible 14 | collection_info = storage.collection.count() 15 | if collection_info == 0: 16 | logger.info("Database is empty but accessible") 17 | 18 | # Verify embedding function is working 19 | test_text = "Database validation test" 20 | embedding = storage.embedding_function([test_text]) 21 | if not embedding or len(embedding) == 0: 22 | return False, "Embedding function is not working properly" 23 | 24 | # Test basic operations 25 | test_id = "test_" + datetime.now().strftime("%Y%m%d_%H%M%S") 26 | 27 | # Test add 28 | storage.collection.add( 29 | documents=[test_text], 30 | metadatas=[{"test": True}], 31 | ids=[test_id] 32 | ) 33 | 34 | # Test query 35 | query_result = storage.collection.query( 36 | query_texts=[test_text], 37 | n_results=1 38 | ) 39 | if not query_result["ids"]: 40 | return False, "Query operation failed" 41 | 42 | # Clean up test data 43 | storage.collection.delete(ids=[test_id]) 44 | 45 | return True, "Database validation successful" 46 | except Exception as e: 47 | logger.error(f"Database validation failed: {str(e)}") 48 | return False, f"Database validation failed: {str(e)}" 49 | 50 | def get_database_stats(storage) -> Dict[str, Any]: 51 | """Get detailed database statistics.""" 52 | try: 53 | count = storage.collection.count() 54 | 55 | # Get collection info 56 | collection_info = { 57 | "total_memories": count, 58 | "embedding_function": storage.embedding_function.__class__.__name__, 59 | "metadata": storage.collection.metadata 60 | } 61 | 62 | # Get storage info 63 | db_path = storage.path 64 | size = 0 65 | for root, dirs, files in os.walk(db_path): 66 | size += sum(os.path.getsize(os.path.join(root, name)) for name in files) 67 | 68 | storage_info = { 69 | "path": db_path, 70 | "size_bytes": size, 71 | "size_mb": round(size / (1024 * 1024), 2) 72 | } 73 | 74 | return { 75 | "collection": collection_info, 76 | "storage": storage_info, 77 | "status": "healthy" 78 | } 79 | except Exception as e: 80 | logger.error(f"Error getting database stats: {str(e)}") 81 | return { 82 | "status": "error", 83 | "error": str(e) 84 | } 85 | 86 | async def repair_database(storage) -> Tuple[bool, str]: 87 | """Attempt to repair database issues.""" 88 | try: 89 | # Validate current state 90 | is_valid, message = await validate_database(storage) 91 | if is_valid: 92 | return True, "Database is already healthy" 93 | 94 | # Backup current embeddings and metadata 95 | try: 96 | existing_data = storage.collection.get() 97 | except Exception as backup_error: 98 | logger.error(f"Could not backup existing data: {str(backup_error)}") 99 | existing_data = None 100 | 101 | # Recreate collection 102 | storage.client.delete_collection("memory_collection") 103 | storage.collection = storage.client.create_collection( 104 | name="memory_collection", 105 | metadata={"hnsw:space": "cosine"}, 106 | embedding_function=storage.embedding_function 107 | ) 108 | 109 | # Restore data if backup was successful 110 | if existing_data and existing_data["ids"]: 111 | storage.collection.add( 112 | documents=existing_data["documents"], 113 | metadatas=existing_data["metadatas"], 114 | ids=existing_data["ids"] 115 | ) 116 | 117 | # Validate repair 118 | is_valid, message = await validate_database(storage) 119 | if is_valid: 120 | return True, "Database successfully repaired" 121 | else: 122 | return False, f"Repair failed: {message}" 123 | 124 | except Exception as e: 125 | logger.error(f"Error repairing database: {str(e)}") 126 | return False, f"Error repairing database: {str(e)}" -------------------------------------------------------------------------------- /src/mcp_memory_service/utils/debug.py: -------------------------------------------------------------------------------- 1 | """Debug utilities for memory service.""" 2 | from typing import Dict, Any, List 3 | import numpy as np 4 | from ..models.memory import Memory, MemoryQueryResult 5 | 6 | def get_raw_embedding(storage, content: str) -> Dict[str, Any]: 7 | """Get raw embedding vector for content.""" 8 | try: 9 | embedding = storage.model.encode(content).tolist() 10 | return { 11 | "status": "success", 12 | "embedding": embedding, 13 | "dimension": len(embedding) 14 | } 15 | except Exception as e: 16 | return { 17 | "status": "error", 18 | "error": str(e) 19 | } 20 | 21 | def check_embedding_model(storage) -> Dict[str, Any]: 22 | """Check if embedding model is loaded and working.""" 23 | try: 24 | test_embedding = storage.model.encode("test").tolist() 25 | return { 26 | "status": "healthy", 27 | "model_loaded": True, 28 | "model_name": storage.model._model_card_vars.get('modelname', 'unknown'), 29 | "embedding_dimension": len(test_embedding) 30 | } 31 | except Exception as e: 32 | return { 33 | "status": "unhealthy", 34 | "error": str(e) 35 | } 36 | 37 | async def debug_retrieve_memory( 38 | storage, 39 | query: str, 40 | n_results: int = 5, 41 | similarity_threshold: float = 0.0 42 | ) -> List[MemoryQueryResult]: 43 | """Retrieve memories with debug information including raw similarity scores.""" 44 | try: 45 | query_embedding = storage.model.encode(query).tolist() 46 | results = storage.collection.query( 47 | query_embeddings=[query_embedding], 48 | n_results=n_results 49 | ) 50 | 51 | memory_results = [] 52 | for i in range(len(results["ids"][0])): 53 | memory = Memory.from_dict( 54 | { 55 | "content": results["documents"][0][i], 56 | **results["metadatas"][0][i] 57 | }, 58 | embedding=results["embeddings"][0][i] if "embeddings" in results else None 59 | ) 60 | similarity = 1 - results["distances"][0][i] 61 | 62 | # Only include results above threshold 63 | if similarity >= similarity_threshold: 64 | memory_results.append( 65 | MemoryQueryResult( 66 | memory=memory, 67 | relevance_score=similarity, 68 | debug_info={ 69 | "raw_similarity": similarity, 70 | "raw_distance": results["distances"][0][i], 71 | "memory_id": results["ids"][0][i] 72 | } 73 | ) 74 | ) 75 | 76 | return memory_results 77 | except Exception as e: 78 | return [] 79 | 80 | async def exact_match_retrieve(storage, content: str) -> List[Memory]: 81 | """Retrieve memories using exact content match.""" 82 | try: 83 | results = storage.collection.get( 84 | where={"content": content} 85 | ) 86 | 87 | memories = [] 88 | for i in range(len(results["ids"])): 89 | memory = Memory.from_dict( 90 | { 91 | "content": results["documents"][i], 92 | **results["metadatas"][i] 93 | }, 94 | embedding=results["embeddings"][i] if "embeddings" in results else None 95 | ) 96 | memories.append(memory) 97 | 98 | return memories 99 | except Exception as e: 100 | return [] -------------------------------------------------------------------------------- /src/mcp_memory_service/utils/hashing.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | from typing import Any, Dict, Optional 4 | 5 | def generate_content_hash(content: str, metadata: Optional[Dict[str, Any]] = None) -> str: 6 | """ 7 | Generate a unique hash for content and metadata. 8 | 9 | This improved version ensures consistent hashing by: 10 | 1. Normalizing content (strip whitespace, lowercase) 11 | 2. Sorting metadata keys 12 | 3. Using a consistent JSON serialization 13 | """ 14 | # Normalize content 15 | normalized_content = content.strip().lower() 16 | 17 | # Create hash content with normalized content 18 | hash_content = normalized_content 19 | 20 | # Add metadata if present 21 | if metadata: 22 | # Filter out timestamp and dynamic fields 23 | static_metadata = { 24 | k: v for k, v in metadata.items() 25 | if k not in ['timestamp', 'content_hash', 'embedding'] 26 | } 27 | if static_metadata: 28 | # Sort keys and use consistent JSON serialization 29 | hash_content += json.dumps(static_metadata, sort_keys=True, ensure_ascii=True) 30 | 31 | # Generate hash 32 | return hashlib.sha256(hash_content.encode('utf-8')).hexdigest() -------------------------------------------------------------------------------- /src/mcp_memory_service/utils/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import logging 3 | 4 | logger = logging.getLogger(__name__) # Add this if not already at top of your file 5 | 6 | def ensure_datetime(ts): 7 | """ 8 | Ensure the input is a datetime object. 9 | 10 | - If ts is a datetime, return as is. 11 | - If ts is a float or int, assume it's a Unix timestamp and convert. 12 | - If ts is a string, try to parse as ISO format. 13 | - If ts is None, return None. 14 | Logs a warning if parsing fails. 15 | """ 16 | if ts is None: 17 | return None 18 | if isinstance(ts, datetime): 19 | return ts 20 | if isinstance(ts, (float, int)): 21 | try: 22 | return datetime.fromtimestamp(ts) 23 | except Exception as e: 24 | logger.warning(f"Failed to convert timestamp {ts} to datetime from float/int: {e}") 25 | return None 26 | if isinstance(ts, str): 27 | try: 28 | return datetime.fromisoformat(ts) 29 | except ValueError as e: 30 | logger.warning(f"Failed to parse string timestamp '{ts}' as ISO datetime: {e}") 31 | return None 32 | logger.warning(f"Unsupported timestamp type: {type(ts)} with value: {ts}") 33 | return None -------------------------------------------------------------------------------- /src/test_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | MCP Memory Service Test Client 3 | Copyright (c) 2024 Heinrich Krupp 4 | Licensed under the MIT License. See LICENSE file in the project root for full license text. 5 | """ 6 | import json 7 | import logging 8 | import sys 9 | import os 10 | from typing import Dict, Any 11 | import threading 12 | import queue 13 | import time 14 | 15 | # Configure logging 16 | logging.basicConfig( 17 | level=logging.DEBUG, 18 | format='%(asctime)s - %(levelname)s - %(message)s', 19 | stream=sys.stderr 20 | ) 21 | logger = logging.getLogger(__name__) 22 | 23 | class MCPTestClient: 24 | def __init__(self): 25 | self.message_id = 0 26 | self.client_name = "test_client" 27 | self.client_version = "0.1.0" 28 | self.protocol_version = "0.1.0" 29 | self.response_queue = queue.Queue() 30 | self._setup_io() 31 | 32 | def _setup_io(self): 33 | """Set up binary mode for Windows.""" 34 | if os.name == 'nt': 35 | import msvcrt 36 | msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) 37 | msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) 38 | sys.stdin.reconfigure(encoding='utf-8') 39 | sys.stdout.reconfigure(encoding='utf-8') 40 | 41 | def get_message_id(self) -> str: 42 | """Generate a unique message ID.""" 43 | self.message_id += 1 44 | return f"msg_{self.message_id}" 45 | 46 | def send_message(self, message: Dict[str, Any], timeout: float = 30.0) -> Dict[str, Any]: 47 | """Send a message and wait for response.""" 48 | try: 49 | message_str = json.dumps(message) + '\n' 50 | logger.debug(f"Sending message: {message_str.strip()}") 51 | 52 | # Write message to stdout 53 | sys.stdout.write(message_str) 54 | sys.stdout.flush() 55 | 56 | # Read response from stdin with timeout 57 | start_time = time.time() 58 | while True: 59 | if time.time() - start_time > timeout: 60 | raise TimeoutError(f"No response received within {timeout} seconds") 61 | 62 | try: 63 | response = sys.stdin.readline() 64 | if response: 65 | logger.debug(f"Received response: {response.strip()}") 66 | return json.loads(response) 67 | except Exception as e: 68 | logger.error(f"Error reading response: {str(e)}") 69 | raise 70 | 71 | time.sleep(0.1) # Small delay to prevent busy waiting 72 | 73 | except Exception as e: 74 | logger.error(f"Error in communication: {str(e)}") 75 | raise 76 | 77 | def test_memory_operations(self): 78 | """Run through a series of test operations.""" 79 | try: 80 | # Initialize connection 81 | logger.info("Initializing connection...") 82 | init_message = { 83 | "jsonrpc": "2.0", 84 | "method": "initialize", 85 | "params": { 86 | "client_name": self.client_name, 87 | "client_version": self.client_version, 88 | "protocol_version": self.protocol_version 89 | }, 90 | "id": self.get_message_id() 91 | } 92 | init_response = self.send_message(init_message) 93 | logger.info(f"Initialization response: {json.dumps(init_response, indent=2)}") 94 | 95 | # List available tools 96 | logger.info("\nListing available tools...") 97 | tools_message = { 98 | "jsonrpc": "2.0", 99 | "method": "list_tools", 100 | "params": {}, 101 | "id": self.get_message_id() 102 | } 103 | tools_response = self.send_message(tools_message) 104 | logger.info(f"Available tools: {json.dumps(tools_response, indent=2)}") 105 | 106 | # Store test memories 107 | test_memories = [ 108 | { 109 | "content": "Remember to update documentation for API changes", 110 | "metadata": { 111 | "tags": ["todo", "documentation", "api"], 112 | "type": "task" 113 | } 114 | }, 115 | { 116 | "content": "Team meeting notes: Discussed new feature rollout plan", 117 | "metadata": { 118 | "tags": ["meeting", "notes", "features"], 119 | "type": "note" 120 | } 121 | } 122 | ] 123 | 124 | logger.info("\nStoring test memories...") 125 | for memory in test_memories: 126 | store_message = { 127 | "jsonrpc": "2.0", 128 | "method": "call_tool", 129 | "params": { 130 | "name": "store_memory", 131 | "arguments": memory 132 | }, 133 | "id": self.get_message_id() 134 | } 135 | store_response = self.send_message(store_message) 136 | logger.info(f"Store response: {json.dumps(store_response, indent=2)}") 137 | 138 | except TimeoutError as e: 139 | logger.error(f"Operation timed out: {str(e)}") 140 | except Exception as e: 141 | logger.error(f"An error occurred: {str(e)}") 142 | raise 143 | 144 | def main(): 145 | client = MCPTestClient() 146 | client.test_memory_operations() 147 | 148 | if __name__ == "__main__": 149 | try: 150 | main() 151 | except KeyboardInterrupt: 152 | logger.info("Test client stopped by user") 153 | except Exception as e: 154 | logger.error(f"Test client failed: {str(e)}") -------------------------------------------------------------------------------- /src/test_management.py: -------------------------------------------------------------------------------- 1 | """ 2 | MCP Memory Service 3 | Copyright (c) 2024 Heinrich Krupp 4 | Licensed under the MIT License. See LICENSE file in the project root for full license text. 5 | """ 6 | import asyncio 7 | import json 8 | import logging 9 | import sys 10 | from datetime import datetime 11 | from mcp.server.models import InitializationOptions 12 | from mcp_memory_service.server import MemoryServer 13 | from mcp_memory_service.utils.hashing import generate_content_hash 14 | from mcp_memory_service.models.memory import Memory 15 | import mcp.types as types 16 | 17 | # Configure logging to output to both file and console 18 | logging.basicConfig( 19 | level=logging.INFO, 20 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 21 | handlers=[ 22 | logging.StreamHandler(sys.stdout), 23 | logging.FileHandler('memory_test.log') 24 | ] 25 | ) 26 | logger = logging.getLogger(__name__) 27 | 28 | def print_separator(title): 29 | print("\n" + "="*50) 30 | print(f" {title} ") 31 | print("="*50 + "\n") 32 | logger.info("\n" + "="*50) 33 | logger.info(f" {title} ") 34 | logger.info("="*50 + "\n") 35 | 36 | async def test_management_features(): 37 | try: 38 | # Initialize the server 39 | print_separator("Initializing Server") 40 | memory_server = MemoryServer() 41 | await memory_server.initialize() 42 | print("Server initialized successfully") 43 | 44 | # 1. Test store_memory 45 | print_separator("Testing store_memory") 46 | test_memories = [ 47 | { 48 | "content": "Important meeting with team tomorrow at 10 AM", 49 | "metadata": { 50 | "type": "calendar", 51 | "tags": "meeting,important" 52 | } 53 | }, 54 | { 55 | "content": "Need to review the ML model performance metrics", 56 | "metadata": { 57 | "type": "todo", 58 | "tags": "ml,review" 59 | } 60 | }, 61 | { 62 | "content": "Remember to backup the database weekly", 63 | "metadata": { 64 | "type": "reminder", 65 | "tags": "backup,database" 66 | } 67 | } 68 | ] 69 | 70 | stored_memories = [] 71 | for memory_data in test_memories: 72 | print(f"Storing memory: {json.dumps(memory_data, indent=2)}") 73 | 74 | # Create Memory object directly 75 | # tags = [tag.strip() for tag in memory_data["metadata"].get("tags", "").split(",") if tag.strip()] 76 | # memory = Memory( 77 | # content=memory_data["content"], 78 | # content_hash=generate_content_hash(memory_data["content"], memory_data["metadata"]), 79 | # tags=tags, 80 | # memory_type=memory_data["metadata"].get("type"), 81 | # metadata=memory_data["metadata"] 82 | # ) 83 | 84 | response = await memory_server.handle_store_memory(memory_data) 85 | 86 | print(f"Store response: [{response[0].text}]") 87 | if "Successfully stored memory" in response[0].text: 88 | tags = [tag.strip() for tag in memory_data["metadata"].get("tags", "").split(",") if tag.strip()] 89 | memory = Memory( 90 | content=memory_data["content"], 91 | content_hash=generate_content_hash(memory_data["content"], memory_data["metadata"]), 92 | tags=tags, 93 | memory_type=memory_data["metadata"].get("type"), 94 | metadata=memory_data["metadata"] 95 | ) 96 | stored_memories.append(memory) 97 | 98 | # 2. Test retrieve_memory 99 | print_separator("Testing retrieve_memory") 100 | query = { 101 | "query": "important meeting tomorrow", 102 | "n_results": 2 103 | } 104 | print(f"Testing retrieval with query: {json.dumps(query, indent=2)}") 105 | results = await memory_server.storage.retrieve(query["query"], query["n_results"]) 106 | print(f"Retrieved {len(results)} results") 107 | for idx, result in enumerate(results): 108 | print(f"Result {idx + 1}:") 109 | print(f" Content: {result.memory.content}") 110 | print(f" Tags: {result.memory.tags}") 111 | print(f" Score: {result.relevance_score}") 112 | 113 | # 3. Test search_by_tag 114 | print_separator("Testing search_by_tag") 115 | test_tag = "meeting" 116 | print(f"Searching for memories with tag: {test_tag}") 117 | tag_results = await memory_server.storage.search_by_tag([test_tag]) 118 | print(f"Found {len(tag_results)} memories with tag '{test_tag}'") 119 | for idx, memory in enumerate(tag_results): 120 | print(f"Memory {idx + 1}:") 121 | print(f" Content: {memory.content}") 122 | print(f" Tags: {memory.tags}") 123 | 124 | # 4. Test get_embedding 125 | print_separator("Testing get_embedding") 126 | test_content = "Test embedding generation" 127 | print(f"Getting embedding for: {test_content}") 128 | embedding = memory_server.storage.model.encode([test_content])[0] 129 | print(f"Generated embedding of size: {len(embedding)}") 130 | 131 | # 5. Test check_embedding_model 132 | print_separator("Testing check_embedding_model") 133 | print("Checking embedding model status") 134 | model_info = { 135 | "status": "healthy" if memory_server.storage.model else "unavailable", 136 | "model_name": "all-MiniLM-L6-v2", 137 | "embedding_dimension": len(embedding) 138 | } 139 | print(f"Model status: {json.dumps(model_info, indent=2)}") 140 | 141 | # 6. Test debug_retrieve 142 | print_separator("Testing debug_retrieve") 143 | debug_query = { 144 | "query": "meeting", 145 | "n_results": 2, 146 | "similarity_threshold": 0.5 147 | } 148 | print(f"Testing debug retrieval with: {json.dumps(debug_query, indent=2)}") 149 | results = await memory_server.storage.retrieve( 150 | debug_query["query"], 151 | debug_query["n_results"] 152 | ) 153 | for idx, result in enumerate(results): 154 | print(f"Debug result {idx + 1}:") 155 | print(f" Content: {result.memory.content}") 156 | print(f" Score: {result.relevance_score}") 157 | print(f" Tags: {result.memory.tags}") 158 | 159 | # 7. Test exact_match_retrieve 160 | print_separator("Testing exact_match_retrieve") 161 | exact_query = stored_memories[0].content if stored_memories else "Important meeting with team tomorrow at 10 AM" 162 | print(f"Testing exact match with: {exact_query}") 163 | matches = [mem for mem in stored_memories if mem.content == exact_query] 164 | print(f"Found {len(matches)} exact matches") 165 | for idx, memory in enumerate(matches): 166 | print(f"Match {idx + 1}:") 167 | print(f" Content: {memory.content}") 168 | print(f" Tags: {memory.tags}") 169 | 170 | # 8. Test cleanup_duplicates 171 | print_separator("Testing cleanup_duplicates") 172 | print("Running duplicate cleanup") 173 | count, message = await memory_server.storage.cleanup_duplicates() 174 | print(f"Cleanup result: {message}") 175 | 176 | # 9. Test delete_by_tag 177 | print_separator("Testing delete_by_tag") 178 | test_tag = "meeting" 179 | print(f"Deleting memories with tag: {test_tag}") 180 | count, message = await memory_server.storage.delete_by_tag(test_tag) 181 | print(f"Delete by tag result: {message}") 182 | 183 | # 10. Clean up by deleting test memories 184 | print_separator("Cleaning up test data") 185 | for memory in stored_memories: 186 | success, message = await memory_server.storage.delete(memory.content_hash) 187 | print(f"Deleted memory {memory.content_hash}: {message}") 188 | 189 | # Final verification 190 | print_separator("Final Verification") 191 | # Verify database is empty or in expected state 192 | tag_results = await memory_server.storage.search_by_tag(["meeting"]) 193 | print(f"Remaining memories with 'meeting' tag: {len(tag_results)}") 194 | 195 | results = await memory_server.storage.retrieve("important meeting", 5) 196 | print(f"Remaining memories matching 'important meeting': {len(results)}") 197 | 198 | except Exception as e: 199 | print(f"Test failed: {str(e)}", file=sys.stderr) 200 | raise 201 | finally: 202 | print("\nTest suite completed") 203 | 204 | if __name__ == "__main__": 205 | # Ensure stdout is flushed immediately 206 | sys.stdout.reconfigure(line_buffering=True) 207 | asyncio.run(test_management_features()) -------------------------------------------------------------------------------- /templates/default.md.j2: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | {% for version in changelog %} 4 | ## {{ version.version }} ({{ version.date }}) 5 | 6 | {% for section, changes in version.sections.items() %} 7 | ### {{ section }} 8 | 9 | {% for change in changes %} 10 | * {{ change.commit.message }}{% if change.commit.breaking %} [BREAKING]{% endif %} 11 | {% endfor %} 12 | 13 | {% endfor %} 14 | {% endfor %} -------------------------------------------------------------------------------- /test_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import mcp 3 | 4 | async def test_memory_service(): 5 | # Connect to memory service 6 | memory = mcp.Peer(name="test_client", service="memory") 7 | print("Connected to MCP memory service") 8 | 9 | # Store a test memory 10 | test_memory = "This is a test memory from our test client" 11 | store_result = await memory.request("store", args={"content": test_memory, "tags": ["test"]}) 12 | print(f"\nStored memory with ID: {store_result}") 13 | 14 | # Retrieve the memory back 15 | retrieve_result = await memory.request("retrieve", args={"query": "test memory", "n_results": 1}) 16 | print("\nRetrieved memory:") 17 | print(f"Content: {retrieve_result[0]['content']}") 18 | print(f"Similarity: {retrieve_result[0]['similarity']:.2f}") 19 | 20 | if __name__ == "__main__": 21 | print("Starting MCP memory service test...") 22 | asyncio.run(test_memory_service()) 23 | 24 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test suite for MCP Memory Service. 3 | This package contains all test modules for verifying the functionality 4 | of the memory service components. 5 | """ -------------------------------------------------------------------------------- /tests/test_database.py: -------------------------------------------------------------------------------- 1 | """ 2 | MCP Memory Service 3 | Copyright (c) 2024 Heinrich Krupp 4 | Licensed under the MIT License. See LICENSE file in the project root for full license text. 5 | """ 6 | """ 7 | Test database operations of the MCP Memory Service. 8 | """ 9 | import pytest 10 | import asyncio 11 | import os 12 | from mcp.server import Server 13 | from mcp.server.models import InitializationOptions 14 | 15 | @pytest.fixture 16 | async def memory_server(): 17 | """Create a test instance of the memory server.""" 18 | server = Server("test-memory") 19 | await server.initialize(InitializationOptions( 20 | server_name="test-memory", 21 | server_version="0.1.0" 22 | )) 23 | yield server 24 | await server.shutdown() 25 | 26 | @pytest.mark.asyncio 27 | async def test_create_backup(memory_server): 28 | """Test database backup creation.""" 29 | # Store some test data 30 | await memory_server.store_memory( 31 | content="Test memory for backup" 32 | ) 33 | 34 | # Create backup 35 | backup_response = await memory_server.create_backup() 36 | 37 | assert backup_response.get("success") is True 38 | assert backup_response.get("backup_path") is not None 39 | assert os.path.exists(backup_response.get("backup_path")) 40 | 41 | @pytest.mark.asyncio 42 | async def test_database_health(memory_server): 43 | """Test database health check functionality.""" 44 | health_status = await memory_server.check_database_health() 45 | 46 | assert health_status is not None 47 | assert "status" in health_status 48 | assert "memory_count" in health_status 49 | assert "database_size" in health_status 50 | 51 | @pytest.mark.asyncio 52 | async def test_optimize_database(memory_server): 53 | """Test database optimization.""" 54 | # Store multiple memories to trigger optimization 55 | for i in range(10): 56 | await memory_server.store_memory( 57 | content=f"Test memory {i}" 58 | ) 59 | 60 | # Run optimization 61 | optimize_response = await memory_server.optimize_db() 62 | 63 | assert optimize_response.get("success") is True 64 | assert "optimized_size" in optimize_response 65 | 66 | @pytest.mark.asyncio 67 | async def test_cleanup_duplicates(memory_server): 68 | """Test duplicate memory cleanup.""" 69 | # Store duplicate memories 70 | duplicate_content = "This is a duplicate memory" 71 | await memory_server.store_memory(content=duplicate_content) 72 | await memory_server.store_memory(content=duplicate_content) 73 | 74 | # Clean up duplicates 75 | cleanup_response = await memory_server.cleanup_duplicates() 76 | 77 | assert cleanup_response.get("success") is True 78 | assert cleanup_response.get("duplicates_removed") >= 1 79 | 80 | # Verify only one copy remains 81 | results = await memory_server.exact_match_retrieve( 82 | content=duplicate_content 83 | ) 84 | assert len(results) == 1 85 | 86 | @pytest.mark.asyncio 87 | async def test_database_persistence(memory_server): 88 | """Test database persistence across server restarts.""" 89 | test_content = "Persistent memory test" 90 | 91 | # Store memory 92 | await memory_server.store_memory(content=test_content) 93 | 94 | # Simulate server restart 95 | await memory_server.shutdown() 96 | await memory_server.initialize(InitializationOptions( 97 | server_name="test-memory", 98 | server_version="0.1.0" 99 | )) 100 | 101 | # Verify memory persists 102 | results = await memory_server.exact_match_retrieve( 103 | content=test_content 104 | ) 105 | assert len(results) == 1 106 | assert results[0] == test_content -------------------------------------------------------------------------------- /tests/test_memory_ops.py: -------------------------------------------------------------------------------- 1 | """ 2 | MCP Memory Service 3 | Copyright (c) 2024 Heinrich Krupp 4 | Licensed under the MIT License. See LICENSE file in the project root for full license text. 5 | """ 6 | """ 7 | Test core memory operations of the MCP Memory Service. 8 | """ 9 | import pytest 10 | import asyncio 11 | from mcp.server import Server 12 | from mcp.server.models import InitializationOptions 13 | 14 | @pytest.fixture 15 | async def memory_server(): 16 | """Create a test instance of the memory server.""" 17 | server = Server("test-memory") 18 | # Initialize with test configuration 19 | await server.initialize(InitializationOptions( 20 | server_name="test-memory", 21 | server_version="0.1.0" 22 | )) 23 | yield server 24 | # Cleanup after tests 25 | await server.shutdown() 26 | 27 | @pytest.mark.asyncio 28 | async def test_store_memory(memory_server): 29 | """Test storing new memory entries.""" 30 | test_content = "The capital of France is Paris" 31 | test_metadata = { 32 | "tags": ["geography", "cities", "europe"], 33 | "type": "fact" 34 | } 35 | 36 | response = await memory_server.store_memory( 37 | content=test_content, 38 | metadata=test_metadata 39 | ) 40 | 41 | assert response is not None 42 | # Add more specific assertions based on expected response format 43 | 44 | @pytest.mark.asyncio 45 | async def test_retrieve_memory(memory_server): 46 | """Test retrieving memories using semantic search.""" 47 | # First store some test data 48 | test_memories = [ 49 | "The capital of France is Paris", 50 | "London is the capital of England", 51 | "Berlin is the capital of Germany" 52 | ] 53 | 54 | for memory in test_memories: 55 | await memory_server.store_memory(content=memory) 56 | 57 | # Test retrieval 58 | query = "What is the capital of France?" 59 | results = await memory_server.retrieve_memory( 60 | query=query, 61 | n_results=1 62 | ) 63 | 64 | assert results is not None 65 | assert len(results) == 1 66 | assert "Paris" in results[0] # The most relevant result should mention Paris 67 | 68 | @pytest.mark.asyncio 69 | async def test_search_by_tag(memory_server): 70 | """Test retrieving memories by tags.""" 71 | # Store memory with tags 72 | await memory_server.store_memory( 73 | content="Paris is beautiful in spring", 74 | metadata={"tags": ["travel", "cities", "europe"]} 75 | ) 76 | 77 | # Search by tags 78 | results = await memory_server.search_by_tag( 79 | tags=["travel", "europe"] 80 | ) 81 | 82 | assert results is not None 83 | assert len(results) > 0 84 | assert "Paris" in results[0] 85 | 86 | @pytest.mark.asyncio 87 | async def test_delete_memory(memory_server): 88 | """Test deleting specific memories.""" 89 | # Store a memory and get its hash 90 | content = "Memory to be deleted" 91 | response = await memory_server.store_memory(content=content) 92 | content_hash = response.get("hash") 93 | 94 | # Delete the memory 95 | delete_response = await memory_server.delete_memory( 96 | content_hash=content_hash 97 | ) 98 | 99 | assert delete_response.get("success") is True 100 | 101 | # Verify memory is deleted 102 | results = await memory_server.exact_match_retrieve(content=content) 103 | assert len(results) == 0 104 | 105 | @pytest.mark.asyncio 106 | async def test_memory_with_empty_content(memory_server): 107 | """Test handling of empty or invalid content.""" 108 | with pytest.raises(ValueError): 109 | await memory_server.store_memory(content="") 110 | 111 | @pytest.mark.asyncio 112 | async def test_memory_with_invalid_tags(memory_server): 113 | """Test handling of invalid tags metadata.""" 114 | with pytest.raises(ValueError): 115 | await memory_server.store_memory( 116 | content="Test content", 117 | metadata={"tags": "invalid"} # Should be a list 118 | ) -------------------------------------------------------------------------------- /tests/test_semantic_search.py: -------------------------------------------------------------------------------- 1 | """ 2 | MCP Memory Service 3 | Copyright (c) 2024 Heinrich Krupp 4 | Licensed under the MIT License. See LICENSE file in the project root for full license text. 5 | """ 6 | """ 7 | Test semantic search functionality of the MCP Memory Service. 8 | """ 9 | import pytest 10 | import asyncio 11 | from mcp.server import Server 12 | from mcp.server.models import InitializationOptions 13 | 14 | @pytest.fixture 15 | async def memory_server(): 16 | """Create a test instance of the memory server.""" 17 | server = Server("test-memory") 18 | await server.initialize(InitializationOptions( 19 | server_name="test-memory", 20 | server_version="0.1.0" 21 | )) 22 | yield server 23 | await server.shutdown() 24 | 25 | @pytest.mark.asyncio 26 | async def test_semantic_similarity(memory_server): 27 | """Test semantic similarity scoring.""" 28 | # Store related memories 29 | memories = [ 30 | "The quick brown fox jumps over the lazy dog", 31 | "A fast auburn fox leaps above a sleepy canine", 32 | "A cat chases a mouse" 33 | ] 34 | 35 | for memory in memories: 36 | await memory_server.store_memory(content=memory) 37 | 38 | # Test semantic retrieval 39 | query = "swift red fox jumping over sleeping dog" 40 | results = await memory_server.debug_retrieve( 41 | query=query, 42 | n_results=2, 43 | similarity_threshold=0.0 # Get all results with scores 44 | ) 45 | 46 | # First two results should be the fox-related memories 47 | assert len(results) >= 2 48 | assert all("fox" in result for result in results[:2]) 49 | 50 | @pytest.mark.asyncio 51 | async def test_similarity_threshold(memory_server): 52 | """Test similarity threshold filtering.""" 53 | await memory_server.store_memory( 54 | content="Python is a programming language" 55 | ) 56 | 57 | # This query is semantically unrelated 58 | results = await memory_server.debug_retrieve( 59 | query="Recipe for chocolate cake", 60 | similarity_threshold=0.8 61 | ) 62 | 63 | assert len(results) == 0 # No results above threshold 64 | 65 | @pytest.mark.asyncio 66 | async def test_exact_match(memory_server): 67 | """Test exact match retrieval.""" 68 | test_content = "This is an exact match test" 69 | await memory_server.store_memory(content=test_content) 70 | 71 | results = await memory_server.exact_match_retrieve( 72 | content=test_content 73 | ) 74 | 75 | assert len(results) == 1 76 | assert results[0] == test_content 77 | 78 | @pytest.mark.asyncio 79 | async def test_semantic_ordering(memory_server): 80 | """Test that results are ordered by semantic similarity.""" 81 | # Store memories with varying relevance 82 | memories = [ 83 | "Machine learning is a subset of artificial intelligence", 84 | "Deep learning uses neural networks", 85 | "A bicycle has two wheels" 86 | ] 87 | 88 | for memory in memories: 89 | await memory_server.store_memory(content=memory) 90 | 91 | query = "What is AI and machine learning?" 92 | results = await memory_server.debug_retrieve( 93 | query=query, 94 | n_results=3, 95 | similarity_threshold=0.0 96 | ) 97 | 98 | # Check ordering 99 | assert "machine learning" in results[0].lower() 100 | assert "bicycle" not in results[0].lower() -------------------------------------------------------------------------------- /tests/test_tag_storage.py: -------------------------------------------------------------------------------- 1 | # tests/test_tag_storage.py 2 | 3 | import asyncio 4 | import pytest 5 | from mcp_memory_service.storage.chroma import ChromaMemoryStorage 6 | from mcp_memory_service.models.memory import Memory 7 | import argparse 8 | 9 | def verify_test_results(tests, expectations): 10 | """Helper function to verify test results against expectations""" 11 | results = [] 12 | for i, (test, expectation) in enumerate(zip(tests, expectations)): 13 | passed = expectation(test) 14 | results.append({ 15 | "test_number": i + 1, 16 | "passed": passed, 17 | "result": test 18 | }) 19 | return results 20 | 21 | async def run_tag_integration_tests(storage): 22 | """Comprehensive test suite for tag handling""" 23 | 24 | # Test Case 1: Array Format Tags 25 | memory1 = await storage.store(Memory( 26 | content="Array format test", 27 | tags=["test1", "test2"] 28 | )) 29 | 30 | # Test Case 2: String Format Tags 31 | memory2 = await storage.store(Memory( 32 | content="String format test", 33 | tags="test2,test3" 34 | )) 35 | 36 | # Test Case 3: Mixed Content Tags 37 | memory3 = await storage.store(Memory( 38 | content="Mixed format test", 39 | tags=["test3", "test4,test5"] 40 | )) 41 | 42 | # Test Case 4: Special Characters 43 | memory4 = await storage.store(Memory( 44 | content="Special chars test", 45 | tags=["test#1", "test@2"] 46 | )) 47 | 48 | # Test Case 5: Empty Tags 49 | memory5 = await storage.store(Memory( 50 | content="Empty tags test", 51 | tags=[] 52 | )) 53 | 54 | # Verification Tests 55 | tests = [ 56 | # Single tag search 57 | await storage.search_by_tag(["test1"]), 58 | 59 | # Multiple tag search 60 | await storage.search_by_tag(["test2", "test3"]), 61 | 62 | # Special character search 63 | await storage.search_by_tag(["test#1"]), 64 | 65 | # Partial tag search (should not match) 66 | await storage.search_by_tag(["test"]), 67 | 68 | # Case sensitivity test 69 | await storage.search_by_tag(["TEST1"]) 70 | ] 71 | 72 | # Expected results 73 | expectations = [ 74 | lambda r: len(r) == 1, # Single tag 75 | lambda r: len(r) == 2, # Multiple tags 76 | lambda r: len(r) == 1, # Special chars 77 | lambda r: len(r) == 0, # Partial match 78 | lambda r: len(r) == 0 # Case sensitivity 79 | ] 80 | 81 | return verify_test_results(tests, expectations) 82 | 83 | @pytest.mark.asyncio 84 | async def test_tag_storage(): 85 | """Main test function that runs all tag storage tests""" 86 | storage = ChromaMemoryStorage("tests/test_db") 87 | 88 | # storage = ChromaMemoryStorage("path/to/your/db") 89 | 90 | # Parse command line arguments 91 | parser = argparse.ArgumentParser(description='Validate memory data tags') 92 | parser.add_argument('--db-path', required=True, help='Path to ChromaDB database') 93 | args = parser.parse_args() 94 | 95 | # Initialize storage with provided path 96 | logger.info(f"Connecting to database at: {args.db_path}") 97 | storage = ChromaMemoryStorage(args.db_path) 98 | 99 | 100 | results = await run_tag_integration_tests(storage) 101 | 102 | # Check if all tests passed 103 | for result in results: 104 | assert result["passed"], f"Test {result['test_number']} failed" 105 | 106 | if __name__ == "__main__": 107 | asyncio.run(test_tag_storage()) -------------------------------------------------------------------------------- /uv_wrapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | UV Wrapper for MCP Memory Service 4 | """ 5 | import os 6 | import sys 7 | import subprocess 8 | import platform 9 | import argparse 10 | 11 | def parse_args(): 12 | parser = argparse.ArgumentParser(description="UV Wrapper for MCP Memory Service") 13 | parser.add_argument("--debug", action="store_true", help="Enable debug mode") 14 | parser.add_argument("--chroma-path", type=str, help="Path to ChromaDB storage") 15 | parser.add_argument("--backups-path", type=str, help="Path to backups storage") 16 | return parser.parse_args() 17 | 18 | def main(): 19 | args = parse_args() 20 | 21 | # Check if UV is installed 22 | try: 23 | subprocess.check_call([sys.executable, '-m', 'uv', '--version'], 24 | stdout=subprocess.DEVNULL, 25 | stderr=subprocess.DEVNULL) 26 | except subprocess.SubprocessError: 27 | print("UV is not installed. Installing UV...") 28 | try: 29 | subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'uv']) 30 | print("UV installed successfully") 31 | except subprocess.SubprocessError as e: 32 | print(f"Failed to install UV: {e}") 33 | print("Please install UV manually: pip install uv") 34 | sys.exit(1) 35 | 36 | # Set environment variables 37 | env = os.environ.copy() 38 | env['UV_ACTIVE'] = '1' 39 | 40 | if args.debug: 41 | env['LOG_LEVEL'] = 'DEBUG' 42 | 43 | if args.chroma_path: 44 | env['MCP_MEMORY_CHROMA_PATH'] = args.chroma_path 45 | 46 | if args.backups_path: 47 | env['MCP_MEMORY_BACKUPS_PATH'] = args.backups_path 48 | 49 | # Run the memory service with UV 50 | uv_cmd = [sys.executable, '-m', 'uv', 'run', 'memory'] 51 | 52 | if args.debug: 53 | uv_cmd.append('--debug') 54 | 55 | try: 56 | subprocess.run(uv_cmd, env=env) 57 | except KeyboardInterrupt: 58 | print("Memory service interrupted") 59 | except Exception as e: 60 | print(f"Error running memory service: {e}") 61 | sys.exit(1) 62 | 63 | if __name__ == "__main__": 64 | main() 65 | --------------------------------------------------------------------------------