├── img ├── server-name.png └── read-function.png ├── .claude └── settings.local.json ├── .aim ├── memory-project-work.jsonl └── memory.jsonl ├── tsconfig.json ├── example.jsonl ├── package.json ├── LICENSE ├── .gitignore ├── README.md ├── pnpm-lock.yaml └── index.ts /img/server-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaneholloman/mcp-knowledge-graph/HEAD/img/server-name.png -------------------------------------------------------------------------------- /img/read-function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaneholloman/mcp-knowledge-graph/HEAD/img/read-function.png -------------------------------------------------------------------------------- /.claude/settings.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(rm:*)", 5 | "Bash(git add:*)", 6 | "Bash(git commit:*)", 7 | "Bash(git push:*)" 8 | ], 9 | "deny": [], 10 | "ask": [] 11 | } 12 | } -------------------------------------------------------------------------------- /.aim/memory-project-work.jsonl: -------------------------------------------------------------------------------- 1 | {"type":"_aim","source":"mcp-knowledge-graph"} 2 | {"type":"entity","name":"Project_Feature_A","entityType":"feature","observations":["Part of local project","Stored in .aim/memory-project-work.jsonl","Isolated from global contexts"]} -------------------------------------------------------------------------------- /.aim/memory.jsonl: -------------------------------------------------------------------------------- 1 | {"type":"_aim","source":"mcp-knowledge-graph"} 2 | {"type":"entity","name":"Local_Project_Entity","entityType":"project_test","observations":["Created in .aim directory","Should be project-local","Testing project detection"]} 3 | {"type":"entity","name":"NPM_Publication_Test","entityType":"milestone","observations":["Successfully published version 1.2.0","Updated Claude config to use npx","All database functionality working perfectly"]} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "rootDir": ".", 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "declaration": true, 12 | "sourceMap": true, 13 | "allowJs": true, 14 | "checkJs": true, 15 | "exactOptionalPropertyTypes": true, 16 | "noUncheckedIndexedAccess": true 17 | }, 18 | "include": [ 19 | "./**/*.ts" 20 | ] 21 | } -------------------------------------------------------------------------------- /example.jsonl: -------------------------------------------------------------------------------- 1 | {"type":"_aim","source":"mcp-knowledge-graph"} 2 | {"type":"entity","name":"Alice_Smith","entityType":"person","observations":["Works as a software engineer","Lives in San Francisco","Speaks Mandarin fluently"]} 3 | {"type":"entity","name":"ML_Project_X","entityType":"project","observations":["Started in 2023","Focus on natural language processing","Currently in development phase"]} 4 | {"type":"entity","name":"TechCorp","entityType":"organization","observations":["Founded in 2010","Specializes in AI development","Headquartered in San Francisco"]} 5 | {"type":"relation","from":"Alice_Smith","to":"ML_Project_X","relationType":"leads"} 6 | {"type":"relation","from":"Alice_Smith","to":"TechCorp","relationType":"works_at"} 7 | {"type":"relation","from":"TechCorp","to":"ML_Project_X","relationType":"owns"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-knowledge-graph", 3 | "version": "1.3.2", 4 | "description": "MCP server enabling persistent memory for AI models through a local knowledge graph", 5 | "license": "MIT", 6 | "author": "Shane Holloman", 7 | "homepage": "https://github.com/shaneholloman/mcp-knowledge-graph", 8 | "bugs": "https://github.com/shaneholloman/mcp-knowledge-graph/issues", 9 | "type": "module", 10 | "engines": { 11 | "node": ">=18.0.0" 12 | }, 13 | "bin": { 14 | "mcp-knowledge-graph": "dist/index.js" 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "scripts": { 20 | "build": "tsc && shx chmod +x dist/*.js", 21 | "prepare": "npm run build", 22 | "watch": "tsc --watch" 23 | }, 24 | "dependencies": { 25 | "@modelcontextprotocol/sdk": "1.0.1", 26 | "minimist": "^1.2.8" 27 | }, 28 | "devDependencies": { 29 | "@types/minimist": "^1.2.5", 30 | "@types/node": "^22.9.3", 31 | "shx": "^0.3.4", 32 | "typescript": "^5.6.2" 33 | } 34 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2025 Shane Holloman 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. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | dist/ 3 | build/ 4 | *.tsbuildinfo 5 | 6 | # Dependencies 7 | node_modules/ 8 | .npm 9 | .pnp.* 10 | .yarn/* 11 | !.yarn/patches 12 | !.yarn/plugins 13 | !.yarn/releases 14 | !.yarn/sdks 15 | !.yarn/versions 16 | 17 | # Logs 18 | logs 19 | *.log 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # Runtime data 25 | pids 26 | *.pid 27 | *.seed 28 | *.pid.lock 29 | 30 | # Testing 31 | coverage/ 32 | .nyc_output/ 33 | 34 | # IDEs and editors 35 | .idea/ 36 | .vscode/* 37 | !.vscode/extensions.json 38 | !.vscode/settings.json 39 | !.vscode/tasks.json 40 | !.vscode/launch.json 41 | *.swp 42 | *.swo 43 | .DS_Store 44 | .env 45 | .env.local 46 | .env.*.local 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Memory files (except examples) 55 | *.jsonl 56 | !example*.jsonl 57 | 58 | # Local documentation 59 | PUBLISHING.md 60 | VERSION_UPDATE.md 61 | 62 | # History files 63 | .history/ 64 | 65 | # Package files 66 | *.tgz 67 | 68 | # OS generated files 69 | .DS_Store 70 | .DS_Store? 71 | ._* 72 | .Spotlight-V100 73 | .Trashes 74 | ehthumbs.db 75 | Thumbs.db 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCP Knowledge Graph 2 | 3 | **Persistent memory for AI models through a local knowledge graph.** 4 | 5 | Store and retrieve information across conversations using entities, relations, and observations. Works with Claude Code/Desktop and any MCP-compatible AI platform. 6 | 7 | ## Why ".aim" and "aim_" prefixes? 8 | 9 | AIM stands for **AI Memory** - the core concept of this system. The three AIM elements provide clear organization and safety: 10 | 11 | - **`.aim` directories**: Keep AI memory files organized and easily identifiable 12 | - **`aim_` tool prefixes**: Group related memory functions together in multi-tool setups 13 | - **`_aim` safety markers**: Each memory file starts with `{"type":"_aim","source":"mcp-knowledge-graph"}` to prevent accidental overwrites of unrelated JSONL files 14 | 15 | This consistent AIM naming makes it obvious which directories, tools, and files belong to the AI memory system. 16 | 17 | ## CRITICAL: Understanding `.aim` dir vs `_aim` file marker 18 | 19 | **Two different things with similar names:** 20 | 21 | - `.aim` = **Project-local directory name** (MUST be named exactly `.aim` for project detection to work) 22 | - `_aim` = **File safety marker** (appears inside JSONL files: `{"type":"_aim","source":"mcp-knowledge-graph"}`) 23 | 24 | **For project-local storage:** 25 | 26 | - Directory MUST be named `.aim` in your project root 27 | - Example: `my-project/.aim/memory.jsonl` 28 | - The system specifically looks for this exact name 29 | 30 | **For global storage (--memory-path):** 31 | 32 | - Can be ANY directory you want 33 | - Examples: `~/yourusername/.aim/`, `~/memories/`, `~/Dropbox/ai-memory/`, `~/Documents/ai-data/` 34 | - Complete flexibility - choose whatever location works for you 35 | 36 | ## Storage Logic 37 | 38 | **File Location Priority:** 39 | 40 | 1. **Project with `.aim`** - Uses `.aim/memory.jsonl` (project-local) 41 | 2. **No project/no .aim** - Uses configured global directory 42 | 3. **Contexts** - Adds suffix: `memory-work.jsonl`, `memory-personal.jsonl` 43 | 44 | **Safety System:** 45 | 46 | - Every memory file starts with `{"type":"_aim","source":"mcp-knowledge-graph"}` 47 | - System refuses to write to files without this marker 48 | - Prevents accidental overwrite of unrelated JSONL files 49 | 50 | ## Master Database Concept 51 | 52 | **The master database is your primary memory store** - used by default when no specific database is requested. It's always named `default` in listings and stored as `memory.jsonl`. 53 | 54 | - **Default Behavior**: All memory operations use the master database unless you specify a different one 55 | - **Always Available**: Exists in both project-local and global locations 56 | - **Primary Storage**: Your main knowledge graph that persists across all conversations 57 | - **Named Databases**: Optional additional databases (`work`, `personal`, `health`) for organizing specific topics 58 | 59 | ## Key Features 60 | 61 | - **Master Database**: Primary memory store used by default for all operations 62 | - **Multiple Databases**: Optional named databases for organizing memories by topic 63 | - **Project Detection**: Automatic project-local memory using `.aim` directories 64 | - **Location Override**: Force operations to use project or global storage 65 | - **Safe Operations**: Built-in protection against overwriting unrelated files 66 | - **Database Discovery**: List all available databases in both locations 67 | 68 | ## Quick Start 69 | 70 | ### Global Memory (Recommended) 71 | 72 | Add to your `claude_desktop_config.json` or `.claude.json`. Two common approaches: 73 | 74 | **Option 1: Default `.aim` directory (simple)** 75 | 76 | ```json 77 | { 78 | "mcpServers": { 79 | "Aim-Memory-Bank": { 80 | "command": "npx", 81 | "args": [ 82 | "-y", 83 | "mcp-knowledge-graph", 84 | "--memory-path", 85 | "/Users/yourusername/.aim" 86 | ] 87 | } 88 | } 89 | } 90 | ``` 91 | 92 | **Option 2: Dropbox/cloud sync (portable)** 93 | 94 | For accessing memories across multiple machines, use a synced folder. This is how the author of this MCP server keeps his own memories: 95 | 96 | ```json 97 | { 98 | "mcpServers": { 99 | "Aim-Memory-Bank": { 100 | "command": "npx", 101 | "args": [ 102 | "-y", 103 | "mcp-knowledge-graph", 104 | "--memory-path", 105 | "/Users/yourusername/Dropbox/ai-memory" 106 | ] 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | This creates memory files in your specified directory: 113 | 114 | - `memory.jsonl` - **Master Database** (default for all operations) 115 | - `memory-work.jsonl` - Work database 116 | - `memory-personal.jsonl` - Personal database 117 | - etc. 118 | 119 | ### Project-Local Memory 120 | 121 | In any project, create a `.aim` directory: 122 | 123 | ```bash 124 | mkdir .aim 125 | ``` 126 | 127 | Now memory tools automatically use `.aim/memory.jsonl` (project-local **master database**) instead of global storage when run from this project. 128 | 129 | ## How AI Uses Databases 130 | 131 | Once configured, AI models use the **master database by default** or can specify named databases with a `context` parameter. New databases are created automatically - no setup required: 132 | 133 | ```json 134 | // Master Database (default - no context needed) 135 | aim_memory_store({ 136 | entities: [{ 137 | name: "John_Doe", 138 | entityType: "person", 139 | observations: ["Met at conference"] 140 | }] 141 | }) 142 | 143 | // Work database 144 | aim_memory_store({ 145 | context: "work", 146 | entities: [{ 147 | name: "Q4_Project", 148 | entityType: "project", 149 | observations: ["Due December 2024"] 150 | }] 151 | }) 152 | 153 | // Personal database 154 | aim_memory_store({ 155 | context: "personal", 156 | entities: [{ 157 | name: "Mom", 158 | entityType: "person", 159 | observations: ["Birthday March 15th"] 160 | }] 161 | }) 162 | 163 | // Master database in specific location 164 | aim_memory_store({ 165 | location: "global", 166 | entities: [{ 167 | name: "Important_Info", 168 | entityType: "reference", 169 | observations: ["Stored in global master database"] 170 | }] 171 | }) 172 | ``` 173 | 174 | ## File Organization 175 | 176 | **Global Setup:** 177 | 178 | ```tree 179 | /Users/yourusername/.aim/ 180 | ├── memory.jsonl # Master Database (default) 181 | ├── memory-work.jsonl # Work database 182 | ├── memory-personal.jsonl # Personal database 183 | └── memory-health.jsonl # Health database 184 | ``` 185 | 186 | **Project Setup:** 187 | 188 | ```tree 189 | my-project/ 190 | ├── .aim/ 191 | │ ├── memory.jsonl # Project Master Database (default) 192 | │ └── memory-work.jsonl # Project Work database 193 | └── src/ 194 | ``` 195 | 196 | ## Available Tools 197 | 198 | - `aim_memory_store` - Store new memories (people, projects, concepts) 199 | - `aim_memory_add_facts` - Add facts to existing memories 200 | - `aim_memory_link` - Link two memories together 201 | - `aim_memory_search` - Search memories by keyword 202 | - `aim_memory_get` - Retrieve specific memories by exact name 203 | - `aim_memory_read_all` - Read all memories in a database 204 | - `aim_memory_list_stores` - List available databases 205 | - `aim_memory_forget` - Forget memories 206 | - `aim_memory_remove_facts` - Remove specific facts from a memory 207 | - `aim_memory_unlink` - Remove links between memories 208 | 209 | ### Parameters 210 | 211 | - `context` (optional) - Specify named database (`work`, `personal`, etc.). Defaults to **master database** 212 | - `location` (optional) - Force `project` or `global` storage location. Defaults to auto-detection 213 | 214 | ## Database Discovery 215 | 216 | Use `aim_memory_list_stores` to see all available databases: 217 | 218 | ```json 219 | { 220 | "project_databases": [ 221 | "default", // Master Database (project-local) 222 | "project-work" // Named database 223 | ], 224 | "global_databases": [ 225 | "default", // Master Database (global) 226 | "work", 227 | "personal", 228 | "health" 229 | ], 230 | "current_location": "project (.aim directory detected)" 231 | } 232 | ``` 233 | 234 | **Key Points:** 235 | 236 | - **"default"** = Master Database in both locations 237 | - **Current location** shows whether you're using project or global storage 238 | - **Master database exists everywhere** - it's your primary memory store 239 | - **Named databases** are optional additions for specific topics 240 | 241 | ## Configuration Examples 242 | 243 | **Important:** Always specify `--memory-path` to control where your memory files are stored. 244 | 245 | **Auto-approve read operations (recommended):** 246 | 247 | ```json 248 | { 249 | "mcpServers": { 250 | "Aim-Memory-Bank": { 251 | "command": "npx", 252 | "args": [ 253 | "-y", 254 | "mcp-knowledge-graph", 255 | "--memory-path", 256 | "/Users/yourusername/.aim" 257 | ], 258 | "autoapprove": [ 259 | "aim_memory_search", 260 | "aim_memory_get", 261 | "aim_memory_read_all", 262 | "aim_memory_list_stores" 263 | ] 264 | } 265 | } 266 | } 267 | ``` 268 | 269 | ## Troubleshooting 270 | 271 | **"File does not contain required _aim safety marker" error:** 272 | 273 | - The file may not belong to this system 274 | - Manual JSONL files need `{"type":"_aim","source":"mcp-knowledge-graph"}` as first line 275 | - If you created the file manually, add the `_aim` marker or delete and let the system recreate it 276 | 277 | **Memories going to unexpected locations:** 278 | 279 | - Check if you're in a project directory with `.aim` folder (uses project-local storage) 280 | - Otherwise uses the configured global `--memory-path` directory 281 | - Use `aim_memory_list_stores` to see all available databases and current location 282 | - Use `ls .aim/` or `ls /Users/yourusername/.aim/` to see your memory files 283 | 284 | **Too many similar databases:** 285 | 286 | - AI models try to use consistent names, but may create variations 287 | - Manually delete unwanted database files if needed 288 | - Encourage AI to use simple, consistent database names 289 | - **Remember**: Master database is always available as the default - named databases are optional 290 | 291 | ## Requirements 292 | 293 | - Node.js 18+ 294 | - MCP-compatible AI platform 295 | 296 | ## License 297 | 298 | MIT 299 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@modelcontextprotocol/sdk': 12 | specifier: 1.0.1 13 | version: 1.0.1 14 | minimist: 15 | specifier: ^1.2.8 16 | version: 1.2.8 17 | devDependencies: 18 | '@types/minimist': 19 | specifier: ^1.2.5 20 | version: 1.2.5 21 | '@types/node': 22 | specifier: ^22.9.3 23 | version: 22.19.3 24 | shx: 25 | specifier: ^0.3.4 26 | version: 0.3.4 27 | typescript: 28 | specifier: ^5.6.2 29 | version: 5.9.3 30 | 31 | packages: 32 | 33 | '@modelcontextprotocol/sdk@1.0.1': 34 | resolution: {integrity: sha512-slLdFaxQJ9AlRg+hw28iiTtGvShAOgOKXcD0F91nUcRYiOMuS9ZBYjcdNZRXW9G5JQ511GRTdUy1zQVZDpJ+4w==} 35 | 36 | '@types/minimist@1.2.5': 37 | resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} 38 | 39 | '@types/node@22.19.3': 40 | resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} 41 | 42 | balanced-match@1.0.2: 43 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 44 | 45 | brace-expansion@1.1.12: 46 | resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} 47 | 48 | bytes@3.1.2: 49 | resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} 50 | engines: {node: '>= 0.8'} 51 | 52 | concat-map@0.0.1: 53 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 54 | 55 | content-type@1.0.5: 56 | resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 57 | engines: {node: '>= 0.6'} 58 | 59 | depd@2.0.0: 60 | resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 61 | engines: {node: '>= 0.8'} 62 | 63 | fs.realpath@1.0.0: 64 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 65 | 66 | function-bind@1.1.2: 67 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 68 | 69 | glob@7.2.3: 70 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 71 | deprecated: Glob versions prior to v9 are no longer supported 72 | 73 | hasown@2.0.2: 74 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 75 | engines: {node: '>= 0.4'} 76 | 77 | http-errors@2.0.1: 78 | resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} 79 | engines: {node: '>= 0.8'} 80 | 81 | iconv-lite@0.7.1: 82 | resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} 83 | engines: {node: '>=0.10.0'} 84 | 85 | inflight@1.0.6: 86 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 87 | deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. 88 | 89 | inherits@2.0.4: 90 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 91 | 92 | interpret@1.4.0: 93 | resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} 94 | engines: {node: '>= 0.10'} 95 | 96 | is-core-module@2.16.1: 97 | resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} 98 | engines: {node: '>= 0.4'} 99 | 100 | minimatch@3.1.2: 101 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 102 | 103 | minimist@1.2.8: 104 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 105 | 106 | once@1.4.0: 107 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 108 | 109 | path-is-absolute@1.0.1: 110 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 111 | engines: {node: '>=0.10.0'} 112 | 113 | path-parse@1.0.7: 114 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 115 | 116 | raw-body@3.0.2: 117 | resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} 118 | engines: {node: '>= 0.10'} 119 | 120 | rechoir@0.6.2: 121 | resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} 122 | engines: {node: '>= 0.10'} 123 | 124 | resolve@1.22.11: 125 | resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} 126 | engines: {node: '>= 0.4'} 127 | hasBin: true 128 | 129 | safer-buffer@2.1.2: 130 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 131 | 132 | setprototypeof@1.2.0: 133 | resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} 134 | 135 | shelljs@0.8.5: 136 | resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} 137 | engines: {node: '>=4'} 138 | hasBin: true 139 | 140 | shx@0.3.4: 141 | resolution: {integrity: sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==} 142 | engines: {node: '>=6'} 143 | hasBin: true 144 | 145 | statuses@2.0.2: 146 | resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} 147 | engines: {node: '>= 0.8'} 148 | 149 | supports-preserve-symlinks-flag@1.0.0: 150 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 151 | engines: {node: '>= 0.4'} 152 | 153 | toidentifier@1.0.1: 154 | resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 155 | engines: {node: '>=0.6'} 156 | 157 | typescript@5.9.3: 158 | resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 159 | engines: {node: '>=14.17'} 160 | hasBin: true 161 | 162 | undici-types@6.21.0: 163 | resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 164 | 165 | unpipe@1.0.0: 166 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} 167 | engines: {node: '>= 0.8'} 168 | 169 | wrappy@1.0.2: 170 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 171 | 172 | zod@3.25.76: 173 | resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} 174 | 175 | snapshots: 176 | 177 | '@modelcontextprotocol/sdk@1.0.1': 178 | dependencies: 179 | content-type: 1.0.5 180 | raw-body: 3.0.2 181 | zod: 3.25.76 182 | 183 | '@types/minimist@1.2.5': {} 184 | 185 | '@types/node@22.19.3': 186 | dependencies: 187 | undici-types: 6.21.0 188 | 189 | balanced-match@1.0.2: {} 190 | 191 | brace-expansion@1.1.12: 192 | dependencies: 193 | balanced-match: 1.0.2 194 | concat-map: 0.0.1 195 | 196 | bytes@3.1.2: {} 197 | 198 | concat-map@0.0.1: {} 199 | 200 | content-type@1.0.5: {} 201 | 202 | depd@2.0.0: {} 203 | 204 | fs.realpath@1.0.0: {} 205 | 206 | function-bind@1.1.2: {} 207 | 208 | glob@7.2.3: 209 | dependencies: 210 | fs.realpath: 1.0.0 211 | inflight: 1.0.6 212 | inherits: 2.0.4 213 | minimatch: 3.1.2 214 | once: 1.4.0 215 | path-is-absolute: 1.0.1 216 | 217 | hasown@2.0.2: 218 | dependencies: 219 | function-bind: 1.1.2 220 | 221 | http-errors@2.0.1: 222 | dependencies: 223 | depd: 2.0.0 224 | inherits: 2.0.4 225 | setprototypeof: 1.2.0 226 | statuses: 2.0.2 227 | toidentifier: 1.0.1 228 | 229 | iconv-lite@0.7.1: 230 | dependencies: 231 | safer-buffer: 2.1.2 232 | 233 | inflight@1.0.6: 234 | dependencies: 235 | once: 1.4.0 236 | wrappy: 1.0.2 237 | 238 | inherits@2.0.4: {} 239 | 240 | interpret@1.4.0: {} 241 | 242 | is-core-module@2.16.1: 243 | dependencies: 244 | hasown: 2.0.2 245 | 246 | minimatch@3.1.2: 247 | dependencies: 248 | brace-expansion: 1.1.12 249 | 250 | minimist@1.2.8: {} 251 | 252 | once@1.4.0: 253 | dependencies: 254 | wrappy: 1.0.2 255 | 256 | path-is-absolute@1.0.1: {} 257 | 258 | path-parse@1.0.7: {} 259 | 260 | raw-body@3.0.2: 261 | dependencies: 262 | bytes: 3.1.2 263 | http-errors: 2.0.1 264 | iconv-lite: 0.7.1 265 | unpipe: 1.0.0 266 | 267 | rechoir@0.6.2: 268 | dependencies: 269 | resolve: 1.22.11 270 | 271 | resolve@1.22.11: 272 | dependencies: 273 | is-core-module: 2.16.1 274 | path-parse: 1.0.7 275 | supports-preserve-symlinks-flag: 1.0.0 276 | 277 | safer-buffer@2.1.2: {} 278 | 279 | setprototypeof@1.2.0: {} 280 | 281 | shelljs@0.8.5: 282 | dependencies: 283 | glob: 7.2.3 284 | interpret: 1.4.0 285 | rechoir: 0.6.2 286 | 287 | shx@0.3.4: 288 | dependencies: 289 | minimist: 1.2.8 290 | shelljs: 0.8.5 291 | 292 | statuses@2.0.2: {} 293 | 294 | supports-preserve-symlinks-flag@1.0.0: {} 295 | 296 | toidentifier@1.0.1: {} 297 | 298 | typescript@5.9.3: {} 299 | 300 | undici-types@6.21.0: {} 301 | 302 | unpipe@1.0.0: {} 303 | 304 | wrappy@1.0.2: {} 305 | 306 | zod@3.25.76: {} 307 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | } from "@modelcontextprotocol/sdk/types.js"; 9 | import { promises as fs } from 'fs'; 10 | import { existsSync } from 'fs'; 11 | import path from 'path'; 12 | import { fileURLToPath } from 'url'; 13 | import { createRequire } from 'module'; 14 | import minimist from 'minimist'; 15 | import { isAbsolute } from 'path'; 16 | 17 | // Read version from package.json - single source of truth 18 | // Path is '../package.json' because compiled code runs from dist/ 19 | const require = createRequire(import.meta.url); 20 | const pkg = require('../package.json') as { version: string; name: string }; 21 | 22 | // Parse args and handle paths safely 23 | const argv = minimist(process.argv.slice(2)); 24 | let memoryPath = argv['memory-path']; 25 | 26 | // If a custom path is provided, ensure it's absolute 27 | if (memoryPath && !isAbsolute(memoryPath)) { 28 | memoryPath = path.resolve(process.cwd(), memoryPath); 29 | } 30 | 31 | // Define the base directory for memory files 32 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 33 | 34 | // Handle memory path - could be a file or directory 35 | let baseMemoryPath: string; 36 | if (memoryPath) { 37 | // If memory-path points to a .jsonl file, use its directory as the base 38 | if (memoryPath.endsWith('.jsonl')) { 39 | baseMemoryPath = path.dirname(memoryPath); 40 | } else { 41 | // Otherwise treat it as a directory 42 | baseMemoryPath = memoryPath; 43 | } 44 | } else { 45 | baseMemoryPath = __dirname; 46 | } 47 | 48 | // Simple marker to identify our files - prevents writing to unrelated JSONL files 49 | const FILE_MARKER = { 50 | type: "_aim", 51 | source: "mcp-knowledge-graph" 52 | }; 53 | 54 | // Project detection - look for common project markers 55 | // .aim is checked first: if it exists, that's an explicit signal for project-local storage 56 | function findProjectRoot(startDir: string = process.cwd()): string | null { 57 | const projectMarkers = ['.aim', '.git', 'package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod']; 58 | let currentDir = startDir; 59 | const maxDepth = 5; 60 | 61 | for (let i = 0; i < maxDepth; i++) { 62 | // Check for project markers 63 | for (const marker of projectMarkers) { 64 | if (existsSync(path.join(currentDir, marker))) { 65 | return currentDir; 66 | } 67 | } 68 | 69 | // Move up one directory 70 | const parentDir = path.dirname(currentDir); 71 | if (parentDir === currentDir) { 72 | // Reached root directory 73 | break; 74 | } 75 | currentDir = parentDir; 76 | } 77 | 78 | return null; 79 | } 80 | 81 | // Function to get memory file path based on context and optional location override 82 | function getMemoryFilePath(context?: string, location?: 'project' | 'global'): string { 83 | const filename = context ? `memory-${context}.jsonl` : 'memory.jsonl'; 84 | 85 | // If location is explicitly specified, use it 86 | if (location === 'global') { 87 | return path.join(baseMemoryPath, filename); 88 | } 89 | 90 | if (location === 'project') { 91 | const projectRoot = findProjectRoot(); 92 | if (projectRoot) { 93 | const aimDir = path.join(projectRoot, '.aim'); 94 | return path.join(aimDir, filename); // Will create .aim if it doesn't exist 95 | } else { 96 | throw new Error('No project detected - cannot use project location'); 97 | } 98 | } 99 | 100 | // Auto-detect logic (existing behavior) 101 | const projectRoot = findProjectRoot(); 102 | if (projectRoot) { 103 | const aimDir = path.join(projectRoot, '.aim'); 104 | if (existsSync(aimDir)) { 105 | return path.join(aimDir, filename); 106 | } 107 | } 108 | 109 | // Fallback to configured base directory 110 | return path.join(baseMemoryPath, filename); 111 | } 112 | 113 | // We are storing our memory using entities, relations, and observations in a graph structure 114 | interface Entity { 115 | name: string; 116 | entityType: string; 117 | observations: string[]; 118 | } 119 | 120 | interface Relation { 121 | from: string; 122 | to: string; 123 | relationType: string; 124 | } 125 | 126 | interface KnowledgeGraph { 127 | entities: Entity[]; 128 | relations: Relation[]; 129 | } 130 | 131 | // Format a knowledge graph as human-readable text 132 | function formatGraphPretty(graph: KnowledgeGraph, context?: string): string { 133 | const lines: string[] = []; 134 | const dbName = context || 'default'; 135 | 136 | lines.push(`=== ${dbName} database ===`); 137 | lines.push(''); 138 | 139 | // Entities section 140 | if (graph.entities.length === 0) { 141 | lines.push('ENTITIES: (none)'); 142 | } else { 143 | lines.push(`ENTITIES (${graph.entities.length}):`); 144 | for (const entity of graph.entities) { 145 | lines.push(` ${entity.name} [${entity.entityType}]`); 146 | for (const obs of entity.observations) { 147 | lines.push(` - ${obs}`); 148 | } 149 | } 150 | } 151 | 152 | lines.push(''); 153 | 154 | // Relations section 155 | if (graph.relations.length === 0) { 156 | lines.push('RELATIONS: (none)'); 157 | } else { 158 | lines.push(`RELATIONS (${graph.relations.length}):`); 159 | for (const rel of graph.relations) { 160 | lines.push(` ${rel.from} --${rel.relationType}--> ${rel.to}`); 161 | } 162 | } 163 | 164 | return lines.join('\n'); 165 | } 166 | 167 | // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph 168 | class KnowledgeGraphManager { 169 | private async loadGraph(context?: string, location?: 'project' | 'global'): Promise { 170 | const filePath = getMemoryFilePath(context, location); 171 | 172 | try { 173 | const data = await fs.readFile(filePath, "utf-8"); 174 | const lines = data.split("\n").filter(line => line.trim() !== ""); 175 | 176 | if (lines.length === 0) { 177 | return { entities: [], relations: [] }; 178 | } 179 | 180 | // Check first line for our file marker 181 | const firstLine = JSON.parse(lines[0]!); 182 | if (firstLine.type !== "_aim" || firstLine.source !== "mcp-knowledge-graph") { 183 | throw new Error(`File ${filePath} does not contain required _aim safety marker. This file may not belong to the knowledge graph system. Expected first line: {"type":"_aim","source":"mcp-knowledge-graph"}`); 184 | } 185 | 186 | // Process remaining lines (skip metadata) 187 | return lines.slice(1).reduce((graph: KnowledgeGraph, line) => { 188 | const item = JSON.parse(line); 189 | if (item.type === "entity") graph.entities.push(item as Entity); 190 | if (item.type === "relation") graph.relations.push(item as Relation); 191 | return graph; 192 | }, { entities: [], relations: [] }); 193 | } catch (error) { 194 | if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { 195 | // File doesn't exist - we'll create it with metadata on first save 196 | return { entities: [], relations: [] }; 197 | } 198 | throw error; 199 | } 200 | } 201 | 202 | private async saveGraph(graph: KnowledgeGraph, context?: string, location?: 'project' | 'global'): Promise { 203 | const filePath = getMemoryFilePath(context, location); 204 | 205 | // Write our simple file marker 206 | 207 | const lines = [ 208 | JSON.stringify(FILE_MARKER), 209 | ...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })), 210 | ...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })), 211 | ]; 212 | 213 | // Ensure directory exists 214 | await fs.mkdir(path.dirname(filePath), { recursive: true }); 215 | 216 | await fs.writeFile(filePath, lines.join("\n")); 217 | } 218 | 219 | async createEntities(entities: Entity[], context?: string, location?: 'project' | 'global'): Promise { 220 | const graph = await this.loadGraph(context, location); 221 | const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name)); 222 | graph.entities.push(...newEntities); 223 | await this.saveGraph(graph, context, location); 224 | return newEntities; 225 | } 226 | 227 | async createRelations(relations: Relation[], context?: string, location?: 'project' | 'global'): Promise { 228 | const graph = await this.loadGraph(context, location); 229 | const newRelations = relations.filter(r => !graph.relations.some(existingRelation => 230 | existingRelation.from === r.from && 231 | existingRelation.to === r.to && 232 | existingRelation.relationType === r.relationType 233 | )); 234 | graph.relations.push(...newRelations); 235 | await this.saveGraph(graph, context, location); 236 | return newRelations; 237 | } 238 | 239 | async addObservations(observations: { entityName: string; contents: string[] }[], context?: string, location?: 'project' | 'global'): Promise<{ entityName: string; addedObservations: string[] }[]> { 240 | const graph = await this.loadGraph(context, location); 241 | const results = observations.map(o => { 242 | const entity = graph.entities.find(e => e.name === o.entityName); 243 | if (!entity) { 244 | throw new Error(`Entity with name ${o.entityName} not found`); 245 | } 246 | const newObservations = o.contents.filter(content => !entity.observations.includes(content)); 247 | entity.observations.push(...newObservations); 248 | return { entityName: o.entityName, addedObservations: newObservations }; 249 | }); 250 | await this.saveGraph(graph, context, location); 251 | return results; 252 | } 253 | 254 | async deleteEntities(entityNames: string[], context?: string, location?: 'project' | 'global'): Promise { 255 | const graph = await this.loadGraph(context, location); 256 | graph.entities = graph.entities.filter(e => !entityNames.includes(e.name)); 257 | graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to)); 258 | await this.saveGraph(graph, context, location); 259 | } 260 | 261 | async deleteObservations(deletions: { entityName: string; observations: string[] }[], context?: string, location?: 'project' | 'global'): Promise { 262 | const graph = await this.loadGraph(context, location); 263 | deletions.forEach(d => { 264 | const entity = graph.entities.find(e => e.name === d.entityName); 265 | if (entity) { 266 | entity.observations = entity.observations.filter(o => !d.observations.includes(o)); 267 | } 268 | }); 269 | await this.saveGraph(graph, context, location); 270 | } 271 | 272 | async deleteRelations(relations: Relation[], context?: string, location?: 'project' | 'global'): Promise { 273 | const graph = await this.loadGraph(context, location); 274 | graph.relations = graph.relations.filter(r => !relations.some(delRelation => 275 | r.from === delRelation.from && 276 | r.to === delRelation.to && 277 | r.relationType === delRelation.relationType 278 | )); 279 | await this.saveGraph(graph, context, location); 280 | } 281 | 282 | async readGraph(context?: string, location?: 'project' | 'global'): Promise { 283 | return this.loadGraph(context, location); 284 | } 285 | 286 | // Very basic search function 287 | async searchNodes(query: string, context?: string, location?: 'project' | 'global'): Promise { 288 | const graph = await this.loadGraph(context, location); 289 | 290 | // Filter entities 291 | const filteredEntities = graph.entities.filter(e => 292 | e.name.toLowerCase().includes(query.toLowerCase()) || 293 | e.entityType.toLowerCase().includes(query.toLowerCase()) || 294 | e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())) 295 | ); 296 | 297 | // Create a Set of filtered entity names for quick lookup 298 | const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); 299 | 300 | // Filter relations to only include those between filtered entities 301 | const filteredRelations = graph.relations.filter(r => 302 | filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) 303 | ); 304 | 305 | const filteredGraph: KnowledgeGraph = { 306 | entities: filteredEntities, 307 | relations: filteredRelations, 308 | }; 309 | 310 | return filteredGraph; 311 | } 312 | 313 | async openNodes(names: string[], context?: string, location?: 'project' | 'global'): Promise { 314 | const graph = await this.loadGraph(context, location); 315 | 316 | // Filter entities 317 | const filteredEntities = graph.entities.filter(e => names.includes(e.name)); 318 | 319 | // Create a Set of filtered entity names for quick lookup 320 | const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); 321 | 322 | // Filter relations to only include those between filtered entities 323 | const filteredRelations = graph.relations.filter(r => 324 | filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) 325 | ); 326 | 327 | const filteredGraph: KnowledgeGraph = { 328 | entities: filteredEntities, 329 | relations: filteredRelations, 330 | }; 331 | 332 | return filteredGraph; 333 | } 334 | 335 | async listDatabases(): Promise<{ project_databases: string[], global_databases: string[], current_location: string }> { 336 | const result = { 337 | project_databases: [] as string[], 338 | global_databases: [] as string[], 339 | current_location: "" 340 | }; 341 | 342 | // Check project-local .aim directory 343 | const projectRoot = findProjectRoot(); 344 | if (projectRoot) { 345 | const aimDir = path.join(projectRoot, '.aim'); 346 | if (existsSync(aimDir)) { 347 | result.current_location = "project (.aim directory detected)"; 348 | try { 349 | const files = await fs.readdir(aimDir); 350 | result.project_databases = files 351 | .filter(file => file.endsWith('.jsonl')) 352 | .map(file => file === 'memory.jsonl' ? 'default' : file.replace('memory-', '').replace('.jsonl', '')) 353 | .sort(); 354 | } catch (error) { 355 | // Directory exists but can't read - ignore 356 | } 357 | } else { 358 | result.current_location = "global (no .aim directory in project)"; 359 | } 360 | } else { 361 | result.current_location = "global (no project detected)"; 362 | } 363 | 364 | // Check global directory 365 | try { 366 | const files = await fs.readdir(baseMemoryPath); 367 | result.global_databases = files 368 | .filter(file => file.endsWith('.jsonl')) 369 | .map(file => file === 'memory.jsonl' ? 'default' : file.replace('memory-', '').replace('.jsonl', '')) 370 | .sort(); 371 | } catch (error) { 372 | // Directory doesn't exist or can't read 373 | result.global_databases = []; 374 | } 375 | 376 | return result; 377 | } 378 | } 379 | 380 | const knowledgeGraphManager = new KnowledgeGraphManager(); 381 | 382 | 383 | // The server instance and tools exposed to AI models 384 | const server = new Server({ 385 | name: pkg.name, 386 | version: pkg.version, 387 | }, { 388 | capabilities: { 389 | tools: {}, 390 | }, 391 | }); 392 | 393 | server.setRequestHandler(ListToolsRequestSchema, async () => { 394 | return { 395 | tools: [ 396 | { 397 | name: "aim_memory_store", 398 | description: `Store new memories. Use this to remember people, projects, concepts, or any information worth persisting. 399 | 400 | AIM (AI Memory) provides persistent memory for AI assistants. The 'aim_memory_' prefix groups all memory tools together. 401 | 402 | WHAT'S STORED: Memories have a name, type (person/project/concept/etc.), and observations (facts about them). 403 | 404 | DATABASES: Use the 'context' parameter to organize memories into separate graphs: 405 | - Leave blank: Uses the master database (default for general information) 406 | - Any name: Creates/uses a named database ('work', 'personal', 'health', 'research', etc.) 407 | - New databases are created automatically - no setup required 408 | - IMPORTANT: Use consistent, simple names - prefer 'work' over 'work-stuff' 409 | 410 | STORAGE LOCATIONS: Files are stored as JSONL (e.g., memory.jsonl, memory-work.jsonl): 411 | - Project-local: .aim directory in project root (auto-detected if exists) 412 | - Global: User's configured --memory-path directory 413 | - Use 'location' parameter to override: 'project' or 'global' 414 | 415 | RETURNS: Array of created entities. 416 | 417 | EXAMPLES: 418 | - Master database (default): aim_memory_store({entities: [{name: "John", entityType: "person", observations: ["Met at conference"]}]}) 419 | - Work database: aim_memory_store({context: "work", entities: [{name: "Q4_Project", entityType: "project", observations: ["Due December 2024"]}]}) 420 | - Master database in global location: aim_memory_store({location: "global", entities: [{name: "John", entityType: "person", observations: ["Met at conference"]}]}) 421 | - Work database in project location: aim_memory_store({context: "work", location: "project", entities: [{name: "Q4_Project", entityType: "project", observations: ["Due December 2024"]}]})`, 422 | inputSchema: { 423 | type: "object", 424 | properties: { 425 | context: { 426 | type: "string", 427 | description: "Optional memory context. Defaults to master database if not specified. Use any descriptive name ('work', 'personal', 'health', 'basket-weaving', etc.) - new contexts created automatically." 428 | }, 429 | location: { 430 | type: "string", 431 | enum: ["project", "global"], 432 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection." 433 | }, 434 | entities: { 435 | type: "array", 436 | items: { 437 | type: "object", 438 | properties: { 439 | name: { type: "string", description: "The name of the entity" }, 440 | entityType: { type: "string", description: "The type of the entity" }, 441 | observations: { 442 | type: "array", 443 | items: { type: "string" }, 444 | description: "An array of observation contents associated with the entity" 445 | }, 446 | }, 447 | required: ["name", "entityType", "observations"], 448 | }, 449 | }, 450 | }, 451 | required: ["entities"], 452 | }, 453 | }, 454 | { 455 | name: "aim_memory_link", 456 | description: `Link two memories together with a relationship. Use this to connect related information. 457 | 458 | RELATION STRUCTURE: Each link has 'from' (subject), 'relationType' (verb), and 'to' (object). 459 | - Use active voice verbs: "manages", "works_at", "knows", "attended", "created" 460 | - Read as: "from relationType to" (e.g., "Alice manages Q4_Project") 461 | - Avoid passive: use "manages" not "is_managed_by" 462 | 463 | IMPORTANT: Both 'from' and 'to' entities must already exist in the same database. 464 | 465 | RETURNS: Array of created relations (duplicates are ignored). 466 | 467 | DATABASE: Relations are created in the specified 'context' database, or master database if not specified. 468 | 469 | EXAMPLES: 470 | - aim_memory_link({relations: [{from: "John", to: "TechConf2024", relationType: "attended"}]}) 471 | - aim_memory_link({context: "work", relations: [{from: "Alice", to: "Q4_Project", relationType: "manages"}]}) 472 | - Multiple: aim_memory_link({relations: [{from: "John", to: "Alice", relationType: "knows"}, {from: "John", to: "Acme_Corp", relationType: "works_at"}]})`, 473 | inputSchema: { 474 | type: "object", 475 | properties: { 476 | context: { 477 | type: "string", 478 | description: "Optional memory context. Relations will be created in the specified context's knowledge graph." 479 | }, 480 | location: { 481 | type: "string", 482 | enum: ["project", "global"], 483 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection." 484 | }, 485 | relations: { 486 | type: "array", 487 | items: { 488 | type: "object", 489 | properties: { 490 | from: { type: "string", description: "The name of the entity where the relation starts" }, 491 | to: { type: "string", description: "The name of the entity where the relation ends" }, 492 | relationType: { type: "string", description: "The type of the relation" }, 493 | }, 494 | required: ["from", "to", "relationType"], 495 | }, 496 | }, 497 | }, 498 | required: ["relations"], 499 | }, 500 | }, 501 | { 502 | name: "aim_memory_add_facts", 503 | description: `Add new facts to an existing memory. Use this to append information to something already stored. 504 | 505 | IMPORTANT: Memory must already exist - use aim_memory_store first. Throws error if not found. 506 | 507 | RETURNS: Array of {entityName, addedObservations} showing what was added (duplicates are ignored). 508 | 509 | DATABASE: Adds to entities in the specified 'context' database, or master database if not specified. 510 | 511 | EXAMPLES: 512 | - aim_memory_add_facts({observations: [{entityName: "John", contents: ["Lives in Seattle", "Works in tech"]}]}) 513 | - aim_memory_add_facts({context: "work", observations: [{entityName: "Q4_Project", contents: ["Behind schedule", "Need more resources"]}]})`, 514 | inputSchema: { 515 | type: "object", 516 | properties: { 517 | context: { 518 | type: "string", 519 | description: "Optional memory context. Observations will be added to entities in the specified context's knowledge graph." 520 | }, 521 | location: { 522 | type: "string", 523 | enum: ["project", "global"], 524 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection." 525 | }, 526 | observations: { 527 | type: "array", 528 | items: { 529 | type: "object", 530 | properties: { 531 | entityName: { type: "string", description: "The name of the entity to add the observations to" }, 532 | contents: { 533 | type: "array", 534 | items: { type: "string" }, 535 | description: "An array of observation contents to add" 536 | }, 537 | }, 538 | required: ["entityName", "contents"], 539 | }, 540 | }, 541 | }, 542 | required: ["observations"], 543 | }, 544 | }, 545 | { 546 | name: "aim_memory_forget", 547 | description: `Forget memories. Removes memories and their associated links. 548 | 549 | DATABASE SELECTION: Entities are deleted from the specified database's knowledge graph. 550 | 551 | LOCATION OVERRIDE: Use the 'location' parameter to force deletion from 'project' (.aim directory) or 'global' (configured directory). Leave blank for auto-detection. 552 | 553 | EXAMPLES: 554 | - Master database (default): aim_memory_forget({entityNames: ["OldProject"]}) 555 | - Work database: aim_memory_forget({context: "work", entityNames: ["CompletedTask", "CancelledMeeting"]}) 556 | - Master database in global location: aim_memory_forget({location: "global", entityNames: ["OldProject"]}) 557 | - Personal database in project location: aim_memory_forget({context: "personal", location: "project", entityNames: ["ExpiredReminder"]})`, 558 | inputSchema: { 559 | type: "object", 560 | properties: { 561 | context: { 562 | type: "string", 563 | description: "Optional memory context. Entities will be deleted from the specified context's knowledge graph." 564 | }, 565 | location: { 566 | type: "string", 567 | enum: ["project", "global"], 568 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection." 569 | }, 570 | entityNames: { 571 | type: "array", 572 | items: { type: "string" }, 573 | description: "An array of entity names to delete" 574 | }, 575 | }, 576 | required: ["entityNames"], 577 | }, 578 | }, 579 | { 580 | name: "aim_memory_remove_facts", 581 | description: `Remove specific facts from a memory. Keeps the memory but removes selected observations. 582 | 583 | DATABASE SELECTION: Observations are deleted from entities within the specified database's knowledge graph. 584 | 585 | LOCATION OVERRIDE: Use the 'location' parameter to force deletion from 'project' (.aim directory) or 'global' (configured directory). Leave blank for auto-detection. 586 | 587 | EXAMPLES: 588 | - Master database (default): aim_memory_remove_facts({deletions: [{entityName: "John", observations: ["Outdated info"]}]}) 589 | - Work database: aim_memory_remove_facts({context: "work", deletions: [{entityName: "Project", observations: ["Old deadline"]}]}) 590 | - Master database in global location: aim_memory_remove_facts({location: "global", deletions: [{entityName: "John", observations: ["Outdated info"]}]}) 591 | - Health database in project location: aim_memory_remove_facts({context: "health", location: "project", deletions: [{entityName: "Exercise", observations: ["Injured knee"]}]})`, 592 | inputSchema: { 593 | type: "object", 594 | properties: { 595 | context: { 596 | type: "string", 597 | description: "Optional memory context. Observations will be deleted from entities in the specified context's knowledge graph." 598 | }, 599 | location: { 600 | type: "string", 601 | enum: ["project", "global"], 602 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection." 603 | }, 604 | deletions: { 605 | type: "array", 606 | items: { 607 | type: "object", 608 | properties: { 609 | entityName: { type: "string", description: "The name of the entity containing the observations" }, 610 | observations: { 611 | type: "array", 612 | items: { type: "string" }, 613 | description: "An array of observations to delete" 614 | }, 615 | }, 616 | required: ["entityName", "observations"], 617 | }, 618 | }, 619 | }, 620 | required: ["deletions"], 621 | }, 622 | }, 623 | { 624 | name: "aim_memory_unlink", 625 | description: `Remove links between memories. Keeps the memories but removes their connections. 626 | 627 | DATABASE SELECTION: Relations are deleted from the specified database's knowledge graph. 628 | 629 | LOCATION OVERRIDE: Use the 'location' parameter to force deletion from 'project' (.aim directory) or 'global' (configured directory). Leave blank for auto-detection. 630 | 631 | EXAMPLES: 632 | - Master database (default): aim_memory_unlink({relations: [{from: "John", to: "OldCompany", relationType: "worked_at"}]}) 633 | - Work database: aim_memory_unlink({context: "work", relations: [{from: "Alice", to: "CancelledProject", relationType: "manages"}]}) 634 | - Master database in global location: aim_memory_unlink({location: "global", relations: [{from: "John", to: "OldCompany", relationType: "worked_at"}]}) 635 | - Personal database in project location: aim_memory_unlink({context: "personal", location: "project", relations: [{from: "Me", to: "OldHobby", relationType: "enjoys"}]})`, 636 | inputSchema: { 637 | type: "object", 638 | properties: { 639 | context: { 640 | type: "string", 641 | description: "Optional memory context. Relations will be deleted from the specified context's knowledge graph." 642 | }, 643 | location: { 644 | type: "string", 645 | enum: ["project", "global"], 646 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection." 647 | }, 648 | relations: { 649 | type: "array", 650 | items: { 651 | type: "object", 652 | properties: { 653 | from: { type: "string", description: "The name of the entity where the relation starts" }, 654 | to: { type: "string", description: "The name of the entity where the relation ends" }, 655 | relationType: { type: "string", description: "The type of the relation" }, 656 | }, 657 | required: ["from", "to", "relationType"], 658 | }, 659 | description: "An array of relations to delete" 660 | }, 661 | }, 662 | required: ["relations"], 663 | }, 664 | }, 665 | { 666 | name: "aim_memory_read_all", 667 | description: `Read all memories in a database. Returns every stored memory and their links. 668 | 669 | FORMAT OPTIONS: 670 | - "json" (default): Structured JSON for programmatic use 671 | - "pretty": Human-readable text format 672 | 673 | DATABASE: Reads from the specified 'context' database, or master database if not specified. 674 | 675 | EXAMPLES: 676 | - aim_memory_read_all({}) - JSON format 677 | - aim_memory_read_all({format: "pretty"}) - Human-readable 678 | - aim_memory_read_all({context: "work", format: "pretty"}) - Work database, pretty`, 679 | inputSchema: { 680 | type: "object", 681 | properties: { 682 | context: { 683 | type: "string", 684 | description: "Optional memory context. Reads from the specified context's knowledge graph or master database if not specified." 685 | }, 686 | location: { 687 | type: "string", 688 | enum: ["project", "global"], 689 | description: "Optional storage location override. 'project' for .aim directory, 'global' for configured directory." 690 | }, 691 | format: { 692 | type: "string", 693 | enum: ["json", "pretty"], 694 | description: "Output format. 'json' (default) for structured data, 'pretty' for human-readable text." 695 | } 696 | }, 697 | }, 698 | }, 699 | { 700 | name: "aim_memory_search", 701 | description: `Search memories by keyword. Use this when you don't know the exact name of what you're looking for. 702 | 703 | WHAT IT SEARCHES: Matches query (case-insensitive) against: 704 | - Memory names (e.g., "John" matches "John_Smith") 705 | - Memory types (e.g., "person" matches all person memories) 706 | - Facts/observations (e.g., "Seattle" matches memories mentioning Seattle) 707 | 708 | VS aim_memory_get: Use aim_memory_search for fuzzy matching. Use aim_memory_get when you know exact names. 709 | 710 | FORMAT OPTIONS: 711 | - "json" (default): Structured JSON for programmatic use 712 | - "pretty": Human-readable text format 713 | 714 | EXAMPLES: 715 | - aim_memory_search({query: "John"}) - JSON format 716 | - aim_memory_search({query: "project", format: "pretty"}) - Human-readable 717 | - aim_memory_search({context: "work", query: "Shane", format: "pretty"})`, 718 | inputSchema: { 719 | type: "object", 720 | properties: { 721 | context: { 722 | type: "string", 723 | description: "Optional database name. Searches within this database or master database if not specified." 724 | }, 725 | location: { 726 | type: "string", 727 | enum: ["project", "global"], 728 | description: "Optional storage location override. 'project' for .aim directory, 'global' for configured directory." 729 | }, 730 | query: { type: "string", description: "Search text to match against entity names, entity types, and observation content (case-insensitive)" }, 731 | format: { 732 | type: "string", 733 | enum: ["json", "pretty"], 734 | description: "Output format. 'json' (default) for structured data, 'pretty' for human-readable text." 735 | } 736 | }, 737 | required: ["query"], 738 | }, 739 | }, 740 | { 741 | name: "aim_memory_get", 742 | description: `Retrieve specific memories by exact name. Use this when you know exactly what you're looking for. 743 | 744 | VS aim_memory_search: Use aim_memory_get for exact name lookup. Use aim_memory_search for fuzzy matching or when you don't know exact names. 745 | 746 | RETURNS: Requested entities and relations between them. Non-existent names are silently ignored. 747 | 748 | FORMAT OPTIONS: 749 | - "json" (default): Structured JSON for programmatic use 750 | - "pretty": Human-readable text format 751 | 752 | EXAMPLES: 753 | - aim_memory_get({names: ["John", "TechConf2024"]}) - JSON format 754 | - aim_memory_get({names: ["Shane"], format: "pretty"}) - Human-readable 755 | - aim_memory_get({context: "work", names: ["Q4_Project"], format: "pretty"})`, 756 | inputSchema: { 757 | type: "object", 758 | properties: { 759 | context: { 760 | type: "string", 761 | description: "Optional memory context. Retrieves entities from the specified context's knowledge graph or master database if not specified." 762 | }, 763 | location: { 764 | type: "string", 765 | enum: ["project", "global"], 766 | description: "Optional storage location override. 'project' for .aim directory, 'global' for configured directory." 767 | }, 768 | names: { 769 | type: "array", 770 | items: { type: "string" }, 771 | description: "An array of entity names to retrieve", 772 | }, 773 | format: { 774 | type: "string", 775 | enum: ["json", "pretty"], 776 | description: "Output format. 'json' (default) for structured data, 'pretty' for human-readable text." 777 | } 778 | }, 779 | required: ["names"], 780 | }, 781 | }, 782 | { 783 | name: "aim_memory_list_stores", 784 | description: `List all available memory databases and show current storage location. 785 | 786 | DATABASE TYPES: 787 | - "default": The master database (memory.jsonl) - used when no context is specified 788 | - Named databases: Created via context parameter (e.g., "work" -> memory-work.jsonl) 789 | 790 | RETURNS: {project_databases: [...], global_databases: [...], current_location: "..."} 791 | - project_databases: Databases in .aim directory (if project detected) 792 | - global_databases: Databases in global --memory-path directory 793 | - current_location: Where operations will default to 794 | 795 | Use this to discover what databases exist before querying them. 796 | 797 | EXAMPLES: 798 | - aim_memory_list_stores() - Shows all available databases and current storage location`, 799 | inputSchema: { 800 | type: "object", 801 | properties: {}, 802 | }, 803 | }, 804 | ], 805 | }; 806 | }); 807 | 808 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 809 | const { name, arguments: args } = request.params; 810 | 811 | if (!args) { 812 | throw new Error(`No arguments provided for tool: ${name}`); 813 | } 814 | 815 | switch (name) { 816 | case "aim_memory_store": 817 | return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[], args.context as string, args.location as 'project' | 'global'), null, 2) }] }; 818 | case "aim_memory_link": 819 | return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[], args.context as string, args.location as 'project' | 'global'), null, 2) }] }; 820 | case "aim_memory_add_facts": 821 | return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[], args.context as string, args.location as 'project' | 'global'), null, 2) }] }; 822 | case "aim_memory_forget": 823 | await knowledgeGraphManager.deleteEntities(args.entityNames as string[], args.context as string, args.location as 'project' | 'global'); 824 | return { content: [{ type: "text", text: "Entities deleted successfully" }] }; 825 | case "aim_memory_remove_facts": 826 | await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[], args.context as string, args.location as 'project' | 'global'); 827 | return { content: [{ type: "text", text: "Observations deleted successfully" }] }; 828 | case "aim_memory_unlink": 829 | await knowledgeGraphManager.deleteRelations(args.relations as Relation[], args.context as string, args.location as 'project' | 'global'); 830 | return { content: [{ type: "text", text: "Relations deleted successfully" }] }; 831 | case "aim_memory_read_all": { 832 | const graph = await knowledgeGraphManager.readGraph(args.context as string, args.location as 'project' | 'global'); 833 | const output = args.format === 'pretty' 834 | ? formatGraphPretty(graph, args.context as string) 835 | : JSON.stringify(graph, null, 2); 836 | return { content: [{ type: "text", text: output }] }; 837 | } 838 | case "aim_memory_search": { 839 | const graph = await knowledgeGraphManager.searchNodes(args.query as string, args.context as string, args.location as 'project' | 'global'); 840 | const output = args.format === 'pretty' 841 | ? formatGraphPretty(graph, args.context as string) 842 | : JSON.stringify(graph, null, 2); 843 | return { content: [{ type: "text", text: output }] }; 844 | } 845 | case "aim_memory_get": { 846 | const graph = await knowledgeGraphManager.openNodes(args.names as string[], args.context as string, args.location as 'project' | 'global'); 847 | const output = args.format === 'pretty' 848 | ? formatGraphPretty(graph, args.context as string) 849 | : JSON.stringify(graph, null, 2); 850 | return { content: [{ type: "text", text: output }] }; 851 | } 852 | case "aim_memory_list_stores": 853 | return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.listDatabases(), null, 2) }] }; 854 | default: 855 | throw new Error(`Unknown tool: ${name}`); 856 | } 857 | }); 858 | 859 | async function main() { 860 | const transport = new StdioServerTransport(); 861 | await server.connect(transport); 862 | console.error("Knowledge Graph MCP Server running on stdio"); 863 | } 864 | 865 | main().catch((error) => { 866 | console.error("Fatal error in main():", error); 867 | process.exit(1); 868 | }); 869 | --------------------------------------------------------------------------------