# Directory Structure
```
├── .aim
│ ├── memory-project-work.jsonl
│ └── memory.jsonl
├── .claude
│ └── settings.local.json
├── .gitignore
├── example.jsonl
├── img
│ ├── read-function.png
│ └── server-name.png
├── index.ts
├── LICENSE
├── package.json
├── README.md
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.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:
--------------------------------------------------------------------------------
```markdown
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 knowledge graph 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 our AI memory system.
16 |
17 | ## Storage Logic
18 |
19 | **File Location Priority:**
20 |
21 | 1. **Project with `.aim`** - Uses `.aim/memory.jsonl` (project-local)
22 | 2. **No project/no .aim** - Uses configured global directory
23 | 3. **Contexts** - Adds suffix: `memory-work.jsonl`, `memory-personal.jsonl`
24 |
25 | **Safety System:**
26 |
27 | - Every memory file starts with `{"type":"_aim","source":"mcp-knowledge-graph"}`
28 | - System refuses to write to files without this marker
29 | - Prevents accidental overwrite of unrelated JSONL files
30 |
31 | ## Master Database Concept
32 |
33 | **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`.
34 |
35 | - **Default Behavior**: All memory operations use the master database unless you specify a different one
36 | - **Always Available**: Exists in both project-local and global locations
37 | - **Primary Storage**: Your main knowledge graph that persists across all conversations
38 | - **Named Databases**: Optional additional databases (`work`, `personal`, `health`) for organizing specific topics
39 |
40 | ## Key Features
41 |
42 | - **Master Database**: Primary memory store used by default for all operations
43 | - **Multiple Databases**: Optional named databases for organizing memories by topic
44 | - **Project Detection**: Automatic project-local memory using `.aim` directories
45 | - **Location Override**: Force operations to use project or global storage
46 | - **Safe Operations**: Built-in protection against overwriting unrelated files
47 | - **Database Discovery**: List all available databases in both locations
48 |
49 | ## Quick Start
50 |
51 | ### Global Memory (Recommended)
52 |
53 | Add to your `claude_desktop_config.json` or `.claude.json`:
54 |
55 | ```json
56 | {
57 | "mcpServers": {
58 | "memory": {
59 | "command": "npx",
60 | "args": [
61 | "-y",
62 | "mcp-knowledge-graph",
63 | "--memory-path",
64 | "/Users/yourusername/.aim/"
65 | ]
66 | }
67 | }
68 | }
69 | ```
70 |
71 | This creates memory files in your specified directory:
72 |
73 | - `memory.jsonl` - **Master Database** (default for all operations)
74 | - `memory-work.jsonl` - Work database
75 | - `memory-personal.jsonl` - Personal database
76 | - etc.
77 |
78 | ### Project-Local Memory
79 |
80 | In any project, create a `.aim` directory:
81 |
82 | ```bash
83 | mkdir .aim
84 | ```
85 |
86 | Now memory tools automatically use `.aim/memory.jsonl` (project-local **master database**) instead of global storage when run from this project.
87 |
88 | ## How AI Uses Databases
89 |
90 | 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:
91 |
92 | ```json
93 | // Master Database (default - no context needed)
94 | aim_create_entities({
95 | entities: [{
96 | name: "John_Doe",
97 | entityType: "person",
98 | observations: ["Met at conference"]
99 | }]
100 | })
101 |
102 | // Work database
103 | aim_create_entities({
104 | context: "work",
105 | entities: [{
106 | name: "Q4_Project",
107 | entityType: "project",
108 | observations: ["Due December 2024"]
109 | }]
110 | })
111 |
112 | // Personal database
113 | aim_create_entities({
114 | context: "personal",
115 | entities: [{
116 | name: "Mom",
117 | entityType: "person",
118 | observations: ["Birthday March 15th"]
119 | }]
120 | })
121 |
122 | // Master database in specific location
123 | aim_create_entities({
124 | location: "global",
125 | entities: [{
126 | name: "Important_Info",
127 | entityType: "reference",
128 | observations: ["Stored in global master database"]
129 | }]
130 | })
131 | ```
132 |
133 | ## File Organization
134 |
135 | **Global Setup:**
136 |
137 | ```tree
138 | /Users/yourusername/.aim/
139 | ├── memory.jsonl # Master Database (default)
140 | ├── memory-work.jsonl # Work database
141 | ├── memory-personal.jsonl # Personal database
142 | └── memory-health.jsonl # Health database
143 | ```
144 |
145 | **Project Setup:**
146 |
147 | ```tree
148 | my-project/
149 | ├── .aim/
150 | │ ├── memory.jsonl # Project Master Database (default)
151 | │ └── memory-work.jsonl # Project Work database
152 | └── src/
153 | ```
154 |
155 | ## Available Tools
156 |
157 | - `aim_create_entities` - Add new people, projects, events
158 | - `aim_create_relations` - Link entities together
159 | - `aim_add_observations` - Add facts to existing entities
160 | - `aim_search_nodes` - Find information by keyword
161 | - `aim_read_graph` - View entire memory
162 | - `aim_open_nodes` - Retrieve specific entities by name
163 | - `aim_list_databases` - Show all available databases and current location
164 | - `aim_delete_entities` - Remove entities
165 | - `aim_delete_observations` - Remove specific facts
166 | - `aim_delete_relations` - Remove connections
167 |
168 | ### Parameters
169 |
170 | - `context` (optional) - Specify named database (`work`, `personal`, etc.). Defaults to **master database**
171 | - `location` (optional) - Force `project` or `global` storage location. Defaults to auto-detection
172 |
173 | ## Database Discovery
174 |
175 | Use `aim_list_databases` to see all available databases:
176 |
177 | ```json
178 | {
179 | "project_databases": [
180 | "default", // Master Database (project-local)
181 | "project-work" // Named database
182 | ],
183 | "global_databases": [
184 | "default", // Master Database (global)
185 | "work",
186 | "personal",
187 | "health"
188 | ],
189 | "current_location": "project (.aim directory detected)"
190 | }
191 | ```
192 |
193 | **Key Points:**
194 |
195 | - **"default"** = Master Database in both locations
196 | - **Current location** shows whether you're using project or global storage
197 | - **Master database exists everywhere** - it's your primary memory store
198 | - **Named databases** are optional additions for specific topics
199 |
200 | ## Configuration Examples
201 |
202 | **Important:** Always specify `--memory-path` to control where your memory files are stored.
203 |
204 | **Home directory:**
205 |
206 | ```json
207 | {
208 | "mcpServers": {
209 | "memory": {
210 | "command": "npx",
211 | "args": [
212 | "-y",
213 | "mcp-knowledge-graph",
214 | "--memory-path",
215 | "/Users/yourusername/.aim"
216 | ]
217 | }
218 | }
219 | }
220 | ```
221 |
222 | **Custom location (e.g., Dropbox):**
223 |
224 | ```json
225 | {
226 | "mcpServers": {
227 | "memory": {
228 | "command": "npx",
229 | "args": [
230 | "-y",
231 | "mcp-knowledge-graph",
232 | "--memory-path",
233 | "/Users/yourusername/Dropbox/.aim"
234 | ]
235 | }
236 | }
237 | }
238 | ```
239 |
240 | **Auto-approve all operations:**
241 |
242 | ```json
243 | {
244 | "mcpServers": {
245 | "memory": {
246 | "command": "npx",
247 | "args": [
248 | "-y",
249 | "mcp-knowledge-graph",
250 | "--memory-path",
251 | "/Users/yourusername/.aim"
252 | ],
253 | "autoapprove": [
254 | "aim_create_entities",
255 | "aim_create_relations",
256 | "aim_add_observations",
257 | "aim_search_nodes",
258 | "aim_read_graph",
259 | "aim_open_nodes",
260 | "aim_list_databases"
261 | ]
262 | }
263 | }
264 | }
265 | ```
266 |
267 | ## Troubleshooting
268 |
269 | **"File does not contain required _aim safety marker" error:**
270 |
271 | - The file may not belong to this system
272 | - Manual JSONL files need `{"type":"_aim","source":"mcp-knowledge-graph"}` as first line
273 | - If you created the file manually, add the `_aim` marker or delete and let the system recreate it
274 |
275 | **Memories going to unexpected locations:**
276 |
277 | - Check if you're in a project directory with `.aim` folder (uses project-local storage)
278 | - Otherwise uses the configured global `--memory-path` directory
279 | - Use `aim_list_databases` to see all available databases and current location
280 | - Use `ls .aim/` or `ls /Users/yourusername/.aim/` to see your memory files
281 |
282 | **Too many similar databases:**
283 |
284 | - AI models try to use consistent names, but may create variations
285 | - Manually delete unwanted database files if needed
286 | - Encourage AI to use simple, consistent database names
287 | - **Remember**: Master database is always available as the default - named databases are optional
288 |
289 | ## Requirements
290 |
291 | - Node.js 18+
292 | - MCP-compatible AI platform
293 |
294 | ## License
295 |
296 | MIT
297 |
```
--------------------------------------------------------------------------------
/.claude/settings.local.json:
--------------------------------------------------------------------------------
```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 | }
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```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 | }
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "mcp-knowledge-graph",
3 | "version": "1.2.0",
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 | }
```
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
```typescript
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 minimist from 'minimist';
14 | import { isAbsolute } from 'path';
15 |
16 | // Parse args and handle paths safely
17 | const argv = minimist(process.argv.slice(2));
18 | let memoryPath = argv['memory-path'];
19 |
20 | // If a custom path is provided, ensure it's absolute
21 | if (memoryPath && !isAbsolute(memoryPath)) {
22 | memoryPath = path.resolve(process.cwd(), memoryPath);
23 | }
24 |
25 | // Define the base directory for memory files
26 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
27 |
28 | // Handle memory path - could be a file or directory
29 | let baseMemoryPath: string;
30 | if (memoryPath) {
31 | // If memory-path points to a .jsonl file, use its directory as the base
32 | if (memoryPath.endsWith('.jsonl')) {
33 | baseMemoryPath = path.dirname(memoryPath);
34 | } else {
35 | // Otherwise treat it as a directory
36 | baseMemoryPath = memoryPath;
37 | }
38 | } else {
39 | baseMemoryPath = __dirname;
40 | }
41 |
42 | // Simple marker to identify our files - prevents writing to unrelated JSONL files
43 | const FILE_MARKER = {
44 | type: "_aim",
45 | source: "mcp-knowledge-graph"
46 | };
47 |
48 | // Project detection - look for common project markers
49 | function findProjectRoot(startDir: string = process.cwd()): string | null {
50 | const projectMarkers = ['.git', 'package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod'];
51 | let currentDir = startDir;
52 | const maxDepth = 5;
53 |
54 | for (let i = 0; i < maxDepth; i++) {
55 | // Check for project markers
56 | for (const marker of projectMarkers) {
57 | if (existsSync(path.join(currentDir, marker))) {
58 | return currentDir;
59 | }
60 | }
61 |
62 | // Move up one directory
63 | const parentDir = path.dirname(currentDir);
64 | if (parentDir === currentDir) {
65 | // Reached root directory
66 | break;
67 | }
68 | currentDir = parentDir;
69 | }
70 |
71 | return null;
72 | }
73 |
74 | // Function to get memory file path based on context and optional location override
75 | function getMemoryFilePath(context?: string, location?: 'project' | 'global'): string {
76 | const filename = context ? `memory-${context}.jsonl` : 'memory.jsonl';
77 |
78 | // If location is explicitly specified, use it
79 | if (location === 'global') {
80 | return path.join(baseMemoryPath, filename);
81 | }
82 |
83 | if (location === 'project') {
84 | const projectRoot = findProjectRoot();
85 | if (projectRoot) {
86 | const aimDir = path.join(projectRoot, '.aim');
87 | return path.join(aimDir, filename); // Will create .aim if it doesn't exist
88 | } else {
89 | throw new Error('No project detected - cannot use project location');
90 | }
91 | }
92 |
93 | // Auto-detect logic (existing behavior)
94 | const projectRoot = findProjectRoot();
95 | if (projectRoot) {
96 | const aimDir = path.join(projectRoot, '.aim');
97 | if (existsSync(aimDir)) {
98 | return path.join(aimDir, filename);
99 | }
100 | }
101 |
102 | // Fallback to configured base directory
103 | return path.join(baseMemoryPath, filename);
104 | }
105 |
106 | // We are storing our memory using entities, relations, and observations in a graph structure
107 | interface Entity {
108 | name: string;
109 | entityType: string;
110 | observations: string[];
111 | }
112 |
113 | interface Relation {
114 | from: string;
115 | to: string;
116 | relationType: string;
117 | }
118 |
119 | interface KnowledgeGraph {
120 | entities: Entity[];
121 | relations: Relation[];
122 | }
123 |
124 | // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
125 | class KnowledgeGraphManager {
126 | private async loadGraph(context?: string, location?: 'project' | 'global'): Promise<KnowledgeGraph> {
127 | const filePath = getMemoryFilePath(context, location);
128 |
129 | try {
130 | const data = await fs.readFile(filePath, "utf-8");
131 | const lines = data.split("\n").filter(line => line.trim() !== "");
132 |
133 | if (lines.length === 0) {
134 | return { entities: [], relations: [] };
135 | }
136 |
137 | // Check first line for our file marker
138 | const firstLine = JSON.parse(lines[0]!);
139 | if (firstLine.type !== "_aim" || firstLine.source !== "mcp-knowledge-graph") {
140 | 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"}`);
141 | }
142 |
143 | // Process remaining lines (skip metadata)
144 | return lines.slice(1).reduce((graph: KnowledgeGraph, line) => {
145 | const item = JSON.parse(line);
146 | if (item.type === "entity") graph.entities.push(item as Entity);
147 | if (item.type === "relation") graph.relations.push(item as Relation);
148 | return graph;
149 | }, { entities: [], relations: [] });
150 | } catch (error) {
151 | if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") {
152 | // File doesn't exist - we'll create it with metadata on first save
153 | return { entities: [], relations: [] };
154 | }
155 | throw error;
156 | }
157 | }
158 |
159 | private async saveGraph(graph: KnowledgeGraph, context?: string, location?: 'project' | 'global'): Promise<void> {
160 | const filePath = getMemoryFilePath(context, location);
161 |
162 | // Write our simple file marker
163 |
164 | const lines = [
165 | JSON.stringify(FILE_MARKER),
166 | ...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })),
167 | ...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })),
168 | ];
169 |
170 | // Ensure directory exists
171 | await fs.mkdir(path.dirname(filePath), { recursive: true });
172 |
173 | await fs.writeFile(filePath, lines.join("\n"));
174 | }
175 |
176 | async createEntities(entities: Entity[], context?: string, location?: 'project' | 'global'): Promise<Entity[]> {
177 | const graph = await this.loadGraph(context, location);
178 | const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name));
179 | graph.entities.push(...newEntities);
180 | await this.saveGraph(graph, context, location);
181 | return newEntities;
182 | }
183 |
184 | async createRelations(relations: Relation[], context?: string, location?: 'project' | 'global'): Promise<Relation[]> {
185 | const graph = await this.loadGraph(context, location);
186 | const newRelations = relations.filter(r => !graph.relations.some(existingRelation =>
187 | existingRelation.from === r.from &&
188 | existingRelation.to === r.to &&
189 | existingRelation.relationType === r.relationType
190 | ));
191 | graph.relations.push(...newRelations);
192 | await this.saveGraph(graph, context, location);
193 | return newRelations;
194 | }
195 |
196 | async addObservations(observations: { entityName: string; contents: string[] }[], context?: string, location?: 'project' | 'global'): Promise<{ entityName: string; addedObservations: string[] }[]> {
197 | const graph = await this.loadGraph(context, location);
198 | const results = observations.map(o => {
199 | const entity = graph.entities.find(e => e.name === o.entityName);
200 | if (!entity) {
201 | throw new Error(`Entity with name ${o.entityName} not found`);
202 | }
203 | const newObservations = o.contents.filter(content => !entity.observations.includes(content));
204 | entity.observations.push(...newObservations);
205 | return { entityName: o.entityName, addedObservations: newObservations };
206 | });
207 | await this.saveGraph(graph, context, location);
208 | return results;
209 | }
210 |
211 | async deleteEntities(entityNames: string[], context?: string, location?: 'project' | 'global'): Promise<void> {
212 | const graph = await this.loadGraph(context, location);
213 | graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
214 | graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
215 | await this.saveGraph(graph, context, location);
216 | }
217 |
218 | async deleteObservations(deletions: { entityName: string; observations: string[] }[], context?: string, location?: 'project' | 'global'): Promise<void> {
219 | const graph = await this.loadGraph(context, location);
220 | deletions.forEach(d => {
221 | const entity = graph.entities.find(e => e.name === d.entityName);
222 | if (entity) {
223 | entity.observations = entity.observations.filter(o => !d.observations.includes(o));
224 | }
225 | });
226 | await this.saveGraph(graph, context, location);
227 | }
228 |
229 | async deleteRelations(relations: Relation[], context?: string, location?: 'project' | 'global'): Promise<void> {
230 | const graph = await this.loadGraph(context, location);
231 | graph.relations = graph.relations.filter(r => !relations.some(delRelation =>
232 | r.from === delRelation.from &&
233 | r.to === delRelation.to &&
234 | r.relationType === delRelation.relationType
235 | ));
236 | await this.saveGraph(graph, context, location);
237 | }
238 |
239 | async readGraph(context?: string, location?: 'project' | 'global'): Promise<KnowledgeGraph> {
240 | return this.loadGraph(context, location);
241 | }
242 |
243 | // Very basic search function
244 | async searchNodes(query: string, context?: string, location?: 'project' | 'global'): Promise<KnowledgeGraph> {
245 | const graph = await this.loadGraph(context, location);
246 |
247 | // Filter entities
248 | const filteredEntities = graph.entities.filter(e =>
249 | e.name.toLowerCase().includes(query.toLowerCase()) ||
250 | e.entityType.toLowerCase().includes(query.toLowerCase()) ||
251 | e.observations.some(o => o.toLowerCase().includes(query.toLowerCase()))
252 | );
253 |
254 | // Create a Set of filtered entity names for quick lookup
255 | const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
256 |
257 | // Filter relations to only include those between filtered entities
258 | const filteredRelations = graph.relations.filter(r =>
259 | filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
260 | );
261 |
262 | const filteredGraph: KnowledgeGraph = {
263 | entities: filteredEntities,
264 | relations: filteredRelations,
265 | };
266 |
267 | return filteredGraph;
268 | }
269 |
270 | async openNodes(names: string[], context?: string, location?: 'project' | 'global'): Promise<KnowledgeGraph> {
271 | const graph = await this.loadGraph(context, location);
272 |
273 | // Filter entities
274 | const filteredEntities = graph.entities.filter(e => names.includes(e.name));
275 |
276 | // Create a Set of filtered entity names for quick lookup
277 | const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
278 |
279 | // Filter relations to only include those between filtered entities
280 | const filteredRelations = graph.relations.filter(r =>
281 | filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
282 | );
283 |
284 | const filteredGraph: KnowledgeGraph = {
285 | entities: filteredEntities,
286 | relations: filteredRelations,
287 | };
288 |
289 | return filteredGraph;
290 | }
291 |
292 | async listDatabases(): Promise<{ project_databases: string[], global_databases: string[], current_location: string }> {
293 | const result = {
294 | project_databases: [] as string[],
295 | global_databases: [] as string[],
296 | current_location: ""
297 | };
298 |
299 | // Check project-local .aim directory
300 | const projectRoot = findProjectRoot();
301 | if (projectRoot) {
302 | const aimDir = path.join(projectRoot, '.aim');
303 | if (existsSync(aimDir)) {
304 | result.current_location = "project (.aim directory detected)";
305 | try {
306 | const files = await fs.readdir(aimDir);
307 | result.project_databases = files
308 | .filter(file => file.endsWith('.jsonl'))
309 | .map(file => file === 'memory.jsonl' ? 'default' : file.replace('memory-', '').replace('.jsonl', ''))
310 | .sort();
311 | } catch (error) {
312 | // Directory exists but can't read - ignore
313 | }
314 | } else {
315 | result.current_location = "global (no .aim directory in project)";
316 | }
317 | } else {
318 | result.current_location = "global (no project detected)";
319 | }
320 |
321 | // Check global directory
322 | try {
323 | const files = await fs.readdir(baseMemoryPath);
324 | result.global_databases = files
325 | .filter(file => file.endsWith('.jsonl'))
326 | .map(file => file === 'memory.jsonl' ? 'default' : file.replace('memory-', '').replace('.jsonl', ''))
327 | .sort();
328 | } catch (error) {
329 | // Directory doesn't exist or can't read
330 | result.global_databases = [];
331 | }
332 |
333 | return result;
334 | }
335 | }
336 |
337 | const knowledgeGraphManager = new KnowledgeGraphManager();
338 |
339 |
340 | // The server instance and tools exposed to AI models
341 | const server = new Server({
342 | name: "mcp-knowledge-graph",
343 | version: "1.0.1",
344 | }, {
345 | capabilities: {
346 | tools: {},
347 | },
348 | },);
349 |
350 | server.setRequestHandler(ListToolsRequestSchema, async () => {
351 | return {
352 | tools: [
353 | {
354 | name: "aim_create_entities",
355 | description: `Create multiple new entities in the knowledge graph.
356 |
357 | DATABASE SELECTION: By default, all memories are stored in the master database. Use the 'context' parameter to organize information into separate knowledge graphs for different areas of life or work.
358 |
359 | STORAGE LOCATION: Files are stored in the user's configured directory, or project-local .aim directory if one exists. Each database creates its own file (e.g., memory-work.jsonl, memory-personal.jsonl).
360 |
361 | LOCATION OVERRIDE: Use the 'location' parameter to force storage in a specific location:
362 | - 'project': Always use project-local .aim directory (creates if needed)
363 | - 'global': Always use global configured directory
364 | - Leave blank: Auto-detect (project if .aim exists, otherwise global)
365 |
366 | WHEN TO USE DATABASES:
367 | - Any descriptive name: 'work', 'personal', 'health', 'research', 'basket-weaving', 'book-club', etc.
368 | - New databases are created automatically - no setup required
369 | - IMPORTANT: Use consistent, simple names - prefer 'work' over 'work-stuff' or 'job-related'
370 | - Common examples: 'work' (professional), 'personal' (private), 'health' (medical), 'research' (academic)
371 | - Leave blank: General information or when unsure (uses master database)
372 |
373 | EXAMPLES:
374 | - Master database (default): aim_create_entities({entities: [{name: "John", entityType: "person", observations: ["Met at conference"]}]})
375 | - Work database: aim_create_entities({context: "work", entities: [{name: "Q4_Project", entityType: "project", observations: ["Due December 2024"]}]})
376 | - Master database in global location: aim_create_entities({location: "global", entities: [{name: "John", entityType: "person", observations: ["Met at conference"]}]})
377 | - Work database in project location: aim_create_entities({context: "work", location: "project", entities: [{name: "Q4_Project", entityType: "project", observations: ["Due December 2024"]}]})`,
378 | inputSchema: {
379 | type: "object",
380 | properties: {
381 | context: {
382 | type: "string",
383 | 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."
384 | },
385 | location: {
386 | type: "string",
387 | enum: ["project", "global"],
388 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection."
389 | },
390 | entities: {
391 | type: "array",
392 | items: {
393 | type: "object",
394 | properties: {
395 | name: { type: "string", description: "The name of the entity" },
396 | entityType: { type: "string", description: "The type of the entity" },
397 | observations: {
398 | type: "array",
399 | items: { type: "string" },
400 | description: "An array of observation contents associated with the entity"
401 | },
402 | },
403 | required: ["name", "entityType", "observations"],
404 | },
405 | },
406 | },
407 | required: ["entities"],
408 | },
409 | },
410 | {
411 | name: "aim_create_relations",
412 | description: `Create multiple new relations between entities in the knowledge graph. Relations should be in active voice.
413 |
414 | DATABASE SELECTION: Relations are created within the specified database's knowledge graph. Entities must exist in the same database.
415 |
416 | LOCATION OVERRIDE: Use the 'location' parameter to force storage in 'project' (.aim directory) or 'global' (configured directory). Leave blank for auto-detection.
417 |
418 | EXAMPLES:
419 | - Master database (default): aim_create_relations({relations: [{from: "John", to: "TechConf2024", relationType: "attended"}]})
420 | - Work database: aim_create_relations({context: "work", relations: [{from: "Alice", to: "Q4_Project", relationType: "manages"}]})
421 | - Master database in global location: aim_create_relations({location: "global", relations: [{from: "John", to: "TechConf2024", relationType: "attended"}]})
422 | - Personal database in project location: aim_create_relations({context: "personal", location: "project", relations: [{from: "Mom", to: "Gardening", relationType: "enjoys"}]})`,
423 | inputSchema: {
424 | type: "object",
425 | properties: {
426 | context: {
427 | type: "string",
428 | description: "Optional memory context. Relations will be created in the specified context's knowledge graph."
429 | },
430 | location: {
431 | type: "string",
432 | enum: ["project", "global"],
433 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection."
434 | },
435 | relations: {
436 | type: "array",
437 | items: {
438 | type: "object",
439 | properties: {
440 | from: { type: "string", description: "The name of the entity where the relation starts" },
441 | to: { type: "string", description: "The name of the entity where the relation ends" },
442 | relationType: { type: "string", description: "The type of the relation" },
443 | },
444 | required: ["from", "to", "relationType"],
445 | },
446 | },
447 | },
448 | required: ["relations"],
449 | },
450 | },
451 | {
452 | name: "aim_add_observations",
453 | description: `Add new observations to existing entities in the knowledge graph.
454 |
455 | DATABASE SELECTION: Observations are added to entities within the specified database's knowledge graph.
456 |
457 | LOCATION OVERRIDE: Use the 'location' parameter to force storage in 'project' (.aim directory) or 'global' (configured directory). Leave blank for auto-detection.
458 |
459 | EXAMPLES:
460 | - Master database (default): aim_add_observations({observations: [{entityName: "John", contents: ["Lives in Seattle", "Works in tech"]}]})
461 | - Work database: aim_add_observations({context: "work", observations: [{entityName: "Q4_Project", contents: ["Behind schedule", "Need more resources"]}]})
462 | - Master database in global location: aim_add_observations({location: "global", observations: [{entityName: "John", contents: ["Lives in Seattle", "Works in tech"]}]})
463 | - Health database in project location: aim_add_observations({context: "health", location: "project", observations: [{entityName: "Daily_Routine", contents: ["30min morning walk", "8 glasses water"]}]})`,
464 | inputSchema: {
465 | type: "object",
466 | properties: {
467 | context: {
468 | type: "string",
469 | description: "Optional memory context. Observations will be added to entities in the specified context's knowledge graph."
470 | },
471 | location: {
472 | type: "string",
473 | enum: ["project", "global"],
474 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection."
475 | },
476 | observations: {
477 | type: "array",
478 | items: {
479 | type: "object",
480 | properties: {
481 | entityName: { type: "string", description: "The name of the entity to add the observations to" },
482 | contents: {
483 | type: "array",
484 | items: { type: "string" },
485 | description: "An array of observation contents to add"
486 | },
487 | },
488 | required: ["entityName", "contents"],
489 | },
490 | },
491 | },
492 | required: ["observations"],
493 | },
494 | },
495 | {
496 | name: "aim_delete_entities",
497 | description: `Delete multiple entities and their associated relations from the knowledge graph.
498 |
499 | DATABASE SELECTION: Entities are deleted from the specified database's knowledge graph.
500 |
501 | LOCATION OVERRIDE: Use the 'location' parameter to force deletion from 'project' (.aim directory) or 'global' (configured directory). Leave blank for auto-detection.
502 |
503 | EXAMPLES:
504 | - Master database (default): aim_delete_entities({entityNames: ["OldProject"]})
505 | - Work database: aim_delete_entities({context: "work", entityNames: ["CompletedTask", "CancelledMeeting"]})
506 | - Master database in global location: aim_delete_entities({location: "global", entityNames: ["OldProject"]})
507 | - Personal database in project location: aim_delete_entities({context: "personal", location: "project", entityNames: ["ExpiredReminder"]})`,
508 | inputSchema: {
509 | type: "object",
510 | properties: {
511 | context: {
512 | type: "string",
513 | description: "Optional memory context. Entities will be deleted from the specified context's knowledge graph."
514 | },
515 | location: {
516 | type: "string",
517 | enum: ["project", "global"],
518 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection."
519 | },
520 | entityNames: {
521 | type: "array",
522 | items: { type: "string" },
523 | description: "An array of entity names to delete"
524 | },
525 | },
526 | required: ["entityNames"],
527 | },
528 | },
529 | {
530 | name: "aim_delete_observations",
531 | description: `Delete specific observations from entities in the knowledge graph.
532 |
533 | DATABASE SELECTION: Observations are deleted from entities within the specified database's knowledge graph.
534 |
535 | LOCATION OVERRIDE: Use the 'location' parameter to force deletion from 'project' (.aim directory) or 'global' (configured directory). Leave blank for auto-detection.
536 |
537 | EXAMPLES:
538 | - Master database (default): aim_delete_observations({deletions: [{entityName: "John", observations: ["Outdated info"]}]})
539 | - Work database: aim_delete_observations({context: "work", deletions: [{entityName: "Project", observations: ["Old deadline"]}]})
540 | - Master database in global location: aim_delete_observations({location: "global", deletions: [{entityName: "John", observations: ["Outdated info"]}]})
541 | - Health database in project location: aim_delete_observations({context: "health", location: "project", deletions: [{entityName: "Exercise", observations: ["Injured knee"]}]})`,
542 | inputSchema: {
543 | type: "object",
544 | properties: {
545 | context: {
546 | type: "string",
547 | description: "Optional memory context. Observations will be deleted from entities in the specified context's knowledge graph."
548 | },
549 | location: {
550 | type: "string",
551 | enum: ["project", "global"],
552 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection."
553 | },
554 | deletions: {
555 | type: "array",
556 | items: {
557 | type: "object",
558 | properties: {
559 | entityName: { type: "string", description: "The name of the entity containing the observations" },
560 | observations: {
561 | type: "array",
562 | items: { type: "string" },
563 | description: "An array of observations to delete"
564 | },
565 | },
566 | required: ["entityName", "observations"],
567 | },
568 | },
569 | },
570 | required: ["deletions"],
571 | },
572 | },
573 | {
574 | name: "aim_delete_relations",
575 | description: `Delete multiple relations from the knowledge graph.
576 |
577 | DATABASE SELECTION: Relations are deleted from the specified database's knowledge graph.
578 |
579 | LOCATION OVERRIDE: Use the 'location' parameter to force deletion from 'project' (.aim directory) or 'global' (configured directory). Leave blank for auto-detection.
580 |
581 | EXAMPLES:
582 | - Master database (default): aim_delete_relations({relations: [{from: "John", to: "OldCompany", relationType: "worked_at"}]})
583 | - Work database: aim_delete_relations({context: "work", relations: [{from: "Alice", to: "CancelledProject", relationType: "manages"}]})
584 | - Master database in global location: aim_delete_relations({location: "global", relations: [{from: "John", to: "OldCompany", relationType: "worked_at"}]})
585 | - Personal database in project location: aim_delete_relations({context: "personal", location: "project", relations: [{from: "Me", to: "OldHobby", relationType: "enjoys"}]})`,
586 | inputSchema: {
587 | type: "object",
588 | properties: {
589 | context: {
590 | type: "string",
591 | description: "Optional memory context. Relations will be deleted from the specified context's knowledge graph."
592 | },
593 | location: {
594 | type: "string",
595 | enum: ["project", "global"],
596 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection."
597 | },
598 | relations: {
599 | type: "array",
600 | items: {
601 | type: "object",
602 | properties: {
603 | from: { type: "string", description: "The name of the entity where the relation starts" },
604 | to: { type: "string", description: "The name of the entity where the relation ends" },
605 | relationType: { type: "string", description: "The type of the relation" },
606 | },
607 | required: ["from", "to", "relationType"],
608 | },
609 | description: "An array of relations to delete"
610 | },
611 | },
612 | required: ["relations"],
613 | },
614 | },
615 | {
616 | name: "aim_read_graph",
617 | description: `Read the entire knowledge graph.
618 |
619 | DATABASE SELECTION: Reads from the specified database or master database if no database is specified.
620 |
621 | LOCATION OVERRIDE: Use the 'location' parameter to force reading from 'project' (.aim directory) or 'global' (configured directory). Leave blank for auto-detection.
622 |
623 | EXAMPLES:
624 | - Master database (default): aim_read_graph({})
625 | - Work database: aim_read_graph({context: "work"})
626 | - Master database in global location: aim_read_graph({location: "global"})
627 | - Personal database in project location: aim_read_graph({context: "personal", location: "project"})`,
628 | inputSchema: {
629 | type: "object",
630 | properties: {
631 | context: {
632 | type: "string",
633 | description: "Optional memory context. Reads from the specified context's knowledge graph or master database if not specified."
634 | },
635 | location: {
636 | type: "string",
637 | enum: ["project", "global"],
638 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection."
639 | }
640 | },
641 | },
642 | },
643 | {
644 | name: "aim_search_nodes",
645 | description: `Search for nodes in the knowledge graph based on a query.
646 |
647 | DATABASE SELECTION: Searches within the specified database or master database if no database is specified.
648 |
649 | LOCATION OVERRIDE: Use the 'location' parameter to force searching in 'project' (.aim directory) or 'global' (configured directory). Leave blank for auto-detection.
650 |
651 | EXAMPLES:
652 | - Master database (default): aim_search_nodes({query: "John"})
653 | - Work database: aim_search_nodes({context: "work", query: "project"})
654 | - Master database in global location: aim_search_nodes({location: "global", query: "John"})
655 | - Personal database in project location: aim_search_nodes({context: "personal", location: "project", query: "family"})`,
656 | inputSchema: {
657 | type: "object",
658 | properties: {
659 | context: {
660 | type: "string",
661 | description: "Optional memory context. Searches within the specified context's knowledge graph or master database if not specified."
662 | },
663 | location: {
664 | type: "string",
665 | enum: ["project", "global"],
666 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection."
667 | },
668 | query: { type: "string", description: "The search query to match against entity names, types, and observation content" },
669 | },
670 | required: ["query"],
671 | },
672 | },
673 | {
674 | name: "aim_open_nodes",
675 | description: `Open specific nodes in the knowledge graph by their names.
676 |
677 | DATABASE SELECTION: Retrieves entities from the specified database or master database if no database is specified.
678 |
679 | LOCATION OVERRIDE: Use the 'location' parameter to force retrieval from 'project' (.aim directory) or 'global' (configured directory). Leave blank for auto-detection.
680 |
681 | EXAMPLES:
682 | - Master database (default): aim_open_nodes({names: ["John", "TechConf2024"]})
683 | - Work database: aim_open_nodes({context: "work", names: ["Q4_Project", "Alice"]})
684 | - Master database in global location: aim_open_nodes({location: "global", names: ["John", "TechConf2024"]})
685 | - Personal database in project location: aim_open_nodes({context: "personal", location: "project", names: ["Mom", "Birthday_Plans"]})`,
686 | inputSchema: {
687 | type: "object",
688 | properties: {
689 | context: {
690 | type: "string",
691 | description: "Optional memory context. Retrieves entities from the specified context's knowledge graph or master database if not specified."
692 | },
693 | location: {
694 | type: "string",
695 | enum: ["project", "global"],
696 | description: "Optional storage location override. 'project' forces project-local .aim directory, 'global' forces global directory. If not specified, uses automatic detection."
697 | },
698 | names: {
699 | type: "array",
700 | items: { type: "string" },
701 | description: "An array of entity names to retrieve",
702 | },
703 | },
704 | required: ["names"],
705 | },
706 | },
707 | {
708 | name: "aim_list_databases",
709 | description: `List all available memory databases in both project and global locations.
710 |
711 | DISCOVERY: Shows which databases exist, where they're stored, and which location is currently active.
712 |
713 | EXAMPLES:
714 | - aim_list_databases() - Shows all available databases and current storage location`,
715 | inputSchema: {
716 | type: "object",
717 | properties: {},
718 | },
719 | },
720 | ],
721 | };
722 | });
723 |
724 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
725 | const { name, arguments: args } = request.params;
726 |
727 | if (!args) {
728 | throw new Error(`No arguments provided for tool: ${name}`);
729 | }
730 |
731 | switch (name) {
732 | case "aim_create_entities":
733 | 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) }] };
734 | case "aim_create_relations":
735 | 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) }] };
736 | case "aim_add_observations":
737 | 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) }] };
738 | case "aim_delete_entities":
739 | await knowledgeGraphManager.deleteEntities(args.entityNames as string[], args.context as string, args.location as 'project' | 'global');
740 | return { content: [{ type: "text", text: "Entities deleted successfully" }] };
741 | case "aim_delete_observations":
742 | await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[], args.context as string, args.location as 'project' | 'global');
743 | return { content: [{ type: "text", text: "Observations deleted successfully" }] };
744 | case "aim_delete_relations":
745 | await knowledgeGraphManager.deleteRelations(args.relations as Relation[], args.context as string, args.location as 'project' | 'global');
746 | return { content: [{ type: "text", text: "Relations deleted successfully" }] };
747 | case "aim_read_graph":
748 | return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.readGraph(args.context as string, args.location as 'project' | 'global'), null, 2) }] };
749 | case "aim_search_nodes":
750 | return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.searchNodes(args.query as string, args.context as string, args.location as 'project' | 'global'), null, 2) }] };
751 | case "aim_open_nodes":
752 | return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.openNodes(args.names as string[], args.context as string, args.location as 'project' | 'global'), null, 2) }] };
753 | case "aim_list_databases":
754 | return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.listDatabases(), null, 2) }] };
755 | default:
756 | throw new Error(`Unknown tool: ${name}`);
757 | }
758 | });
759 |
760 | async function main() {
761 | const transport = new StdioServerTransport();
762 | await server.connect(transport);
763 | console.error("Knowledge Graph MCP Server running on stdio");
764 | }
765 |
766 | main().catch((error) => {
767 | console.error("Fatal error in main():", error);
768 | process.exit(1);
769 | });
770 |
```