diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index 668f071fba..0000000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# MCP Reference Server Development Guide
-
-## Contributing Guidelines
-
-Before making updates to this repo, thoroughly review the CONTRIBUTING.md guide at the root of this repo.
-
-## Testing
-
-Use vitest when configuring or adding tests for servers implemented in typescript.
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index 6fc464b3ba..0000000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1,2 +0,0 @@
-@./AGENTS.md
-
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7a10a22f93..932b25991a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -21,6 +21,10 @@ We're more selective about:
We don't accept:
- **New server implementations** — We encourage you to publish them yourself, and link to them from the README.
+## Testing
+
+When adding or configuring tests for servers implemented in TypeScript, use **vitest** as the test framework. Vitest provides better ESM support, faster test execution, and a more modern testing experience.
+
## Documentation
Improvements to existing documentation is welcome - although generally we'd prefer ergonomic improvements than documenting pain points if possible!
diff --git a/README.md b/README.md
index 8301247b8d..11938c8a14 100644
--- a/README.md
+++ b/README.md
@@ -393,6 +393,7 @@ Official integrations are maintained by companies building production ready MCP
-
**[Polymarket](https://github.com/ozgureyilmaz/polymarket-mcp)** - Real-time prediction market data from Polymarket - search markets, analyze prices, identify trading opportunities.
-
**[Port IO](https://github.com/port-labs/port-mcp-server)** - Access and manage your software catalog to improve service quality and compliance.
- **[PostHog](https://github.com/posthog/mcp)** - Interact with PostHog analytics, feature flags, error tracking and more with the official PostHog MCP server.
+-
**[PostIdentity](https://github.com/PostIdentity/mcp-server)** - Generate AI-powered social media posts from any AI assistant. Manage identities, create posts, track referrals, and browse marketplace templates, powered by [PostIdentity](https://postidentity.com).
- **[Postman API](https://github.com/postmanlabs/postman-api-mcp)** - Manage your Postman resources using the [Postman API](https://www.postman.com/postman/postman-public-workspace/collection/i2uqzpp/postman-api).
-
**[Powerdrill](https://github.com/powerdrillai/powerdrill-mcp)** - An MCP server that provides tools to interact with Powerdrill datasets, enabling smart AI data analysis and insights.
-
**[Prisma](https://www.prisma.io/docs/postgres/mcp-server)** - Create and manage Prisma Postgres databases
@@ -525,6 +526,7 @@ Official integrations are maintained by companies building production ready MCP
- **[ZettelkastenSpace](https://github.com/joshylchen/zettelkasten_space)** - Built on the proven [Zettelkasten](https://www.zettelkasten.space/) method, enhanced with Claude Desktop integration via Model Context Protocol
-
**[Zine](https://www.zine.ai)** - Your memory, everywhere AI goes. Think iPhoto for your knowledge - upload and curate. Like ChatGPT but portable - context that travels with you.
-
**[ZIZAI Recruitment](https://github.com/zaiwork/mcp)** - Interact with the next-generation intelligent recruitment platform for employees and employers, powered by [ZIZAI Recruitment](https://zizai.work).
+
### 🌎 Community Servers
A growing set of community-developed and maintained servers demonstrates various applications of MCP across different domains.
@@ -1472,6 +1474,7 @@ Additional resources on MCP.
-
**[mkinf](https://mkinf.io)** - An Open Source registry of hosted MCP Servers to accelerate AI agent workflows.
- **[Open-Sourced MCP Servers Directory](https://github.com/chatmcp/mcp-directory)** - A curated list of MCP servers by **[mcpso](https://mcp.so)**
-
**[OpenTools](https://opentools.com)** - An open registry for finding, installing, and building with MCP servers by **[opentoolsteam](https://github.com/opentoolsteam)**
+- **[Programmatic MCP Prototype](https://github.com/domdomegg/programmatic-mcp-prototype)** - Experimental agent prototype demonstrating programmatic MCP tool composition, progressive tool discovery, state persistence, and skill building through TypeScript code execution by **[Adam Jones](https://github.com/domdomegg)**
- **[PulseMCP](https://www.pulsemcp.com)** ([API](https://www.pulsemcp.com/api)) - Community hub & weekly newsletter for discovering MCP servers, clients, articles, and news by **[Tadas Antanavicius](https://github.com/tadasant)**, **[Mike Coughlin](https://github.com/macoughl)**, and **[Ravina Patel](https://github.com/ravinahp)**
- **[r/mcp](https://www.reddit.com/r/mcp)** – A Reddit community dedicated to MCP by **[Frank Fiegel](https://github.com/punkpeye)**
- **[r/modelcontextprotocol](https://www.reddit.com/r/modelcontextprotocol)** – A Model Context Protocol community Reddit page - discuss ideas, get answers to your questions, network with like-minded people, and showcase your projects! by **[Alex Andru](https://github.com/QuantGeekDev)**
diff --git a/package-lock.json b/package-lock.json
index f0fc649649..80a20fb57a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4284,8 +4284,10 @@
},
"devDependencies": {
"@types/node": "^22",
+ "@vitest/coverage-v8": "^2.1.8",
"shx": "^0.3.4",
- "typescript": "^5.6.2"
+ "typescript": "^5.6.2",
+ "vitest": "^2.1.8"
}
},
"src/postgres": {
diff --git a/src/filesystem/__tests__/path-utils.test.ts b/src/filesystem/__tests__/path-utils.test.ts
index b03215a9e3..868d8f08af 100644
--- a/src/filesystem/__tests__/path-utils.test.ts
+++ b/src/filesystem/__tests__/path-utils.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, afterEach } from 'vitest';
import { normalizePath, expandHome, convertToWindowsPath } from '../path-utils.js';
describe('Path Utilities', () => {
@@ -196,11 +196,8 @@ describe('Path Utilities', () => {
});
it('returns normalized non-Windows/WSL/Unix-style Windows paths as is after basic normalization', () => {
- // Relative path
- const relativePath = 'some/relative/path';
- expect(normalizePath(relativePath)).toBe(relativePath.replace(/\//g, '\\'));
-
// A path that looks somewhat absolute but isn't a drive or recognized Unix root for Windows conversion
+ // These paths should be preserved as-is (not converted to Windows C:\ format or WSL format)
const otherAbsolutePath = '\\someserver\\share\\file';
expect(normalizePath(otherAbsolutePath)).toBe(otherAbsolutePath);
});
@@ -350,5 +347,19 @@ describe('Path Utilities', () => {
expect(result).not.toContain('C:');
expect(result).not.toContain('\\');
});
+
+ it('should handle relative path slash conversion based on platform', () => {
+ // This test verifies platform-specific behavior naturally without mocking
+ // On Windows: forward slashes converted to backslashes
+ // On Linux/Unix: forward slashes preserved
+ const relativePath = 'some/relative/path';
+ const result = normalizePath(relativePath);
+
+ if (originalPlatform === 'win32') {
+ expect(result).toBe('some\\relative\\path');
+ } else {
+ expect(result).toBe('some/relative/path');
+ }
+ });
});
});
diff --git a/src/memory/__tests__/file-path.test.ts b/src/memory/__tests__/file-path.test.ts
new file mode 100644
index 0000000000..d1a16e4600
--- /dev/null
+++ b/src/memory/__tests__/file-path.test.ts
@@ -0,0 +1,156 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import { promises as fs } from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { ensureMemoryFilePath, defaultMemoryPath } from '../index.js';
+
+describe('ensureMemoryFilePath', () => {
+ const testDir = path.dirname(fileURLToPath(import.meta.url));
+ const oldMemoryPath = path.join(testDir, '..', 'memory.json');
+ const newMemoryPath = path.join(testDir, '..', 'memory.jsonl');
+
+ let originalEnv: string | undefined;
+
+ beforeEach(() => {
+ // Save original environment variable
+ originalEnv = process.env.MEMORY_FILE_PATH;
+ // Delete environment variable
+ delete process.env.MEMORY_FILE_PATH;
+ });
+
+ afterEach(async () => {
+ // Restore original environment variable
+ if (originalEnv !== undefined) {
+ process.env.MEMORY_FILE_PATH = originalEnv;
+ } else {
+ delete process.env.MEMORY_FILE_PATH;
+ }
+
+ // Clean up test files
+ try {
+ await fs.unlink(oldMemoryPath);
+ } catch {
+ // Ignore if file doesn't exist
+ }
+ try {
+ await fs.unlink(newMemoryPath);
+ } catch {
+ // Ignore if file doesn't exist
+ }
+ });
+
+ describe('with MEMORY_FILE_PATH environment variable', () => {
+ it('should return absolute path when MEMORY_FILE_PATH is absolute', async () => {
+ const absolutePath = '/tmp/custom-memory.jsonl';
+ process.env.MEMORY_FILE_PATH = absolutePath;
+
+ const result = await ensureMemoryFilePath();
+
+ expect(result).toBe(absolutePath);
+ });
+
+ it('should convert relative path to absolute when MEMORY_FILE_PATH is relative', async () => {
+ const relativePath = 'custom-memory.jsonl';
+ process.env.MEMORY_FILE_PATH = relativePath;
+
+ const result = await ensureMemoryFilePath();
+
+ expect(path.isAbsolute(result)).toBe(true);
+ expect(result).toContain('custom-memory.jsonl');
+ });
+
+ it('should handle Windows absolute paths', async () => {
+ const windowsPath = 'C:\\temp\\memory.jsonl';
+ process.env.MEMORY_FILE_PATH = windowsPath;
+
+ const result = await ensureMemoryFilePath();
+
+ // On Windows, should return as-is; on Unix, will be treated as relative
+ if (process.platform === 'win32') {
+ expect(result).toBe(windowsPath);
+ } else {
+ expect(path.isAbsolute(result)).toBe(true);
+ }
+ });
+ });
+
+ describe('without MEMORY_FILE_PATH environment variable', () => {
+ it('should return default path when no files exist', async () => {
+ const result = await ensureMemoryFilePath();
+
+ expect(result).toBe(defaultMemoryPath);
+ });
+
+ it('should migrate from memory.json to memory.jsonl when only old file exists', async () => {
+ // Create old memory.json file
+ await fs.writeFile(oldMemoryPath, '{"test":"data"}');
+
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
+
+ const result = await ensureMemoryFilePath();
+
+ expect(result).toBe(defaultMemoryPath);
+
+ // Verify migration happened
+ const newFileExists = await fs.access(newMemoryPath).then(() => true).catch(() => false);
+ const oldFileExists = await fs.access(oldMemoryPath).then(() => true).catch(() => false);
+
+ expect(newFileExists).toBe(true);
+ expect(oldFileExists).toBe(false);
+
+ // Verify console messages
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ expect.stringContaining('DETECTED: Found legacy memory.json file')
+ );
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ expect.stringContaining('COMPLETED: Successfully migrated')
+ );
+
+ consoleErrorSpy.mockRestore();
+ });
+
+ it('should use new file when both old and new files exist', async () => {
+ // Create both files
+ await fs.writeFile(oldMemoryPath, '{"old":"data"}');
+ await fs.writeFile(newMemoryPath, '{"new":"data"}');
+
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
+
+ const result = await ensureMemoryFilePath();
+
+ expect(result).toBe(defaultMemoryPath);
+
+ // Verify no migration happened (both files should still exist)
+ const newFileExists = await fs.access(newMemoryPath).then(() => true).catch(() => false);
+ const oldFileExists = await fs.access(oldMemoryPath).then(() => true).catch(() => false);
+
+ expect(newFileExists).toBe(true);
+ expect(oldFileExists).toBe(true);
+
+ // Verify no console messages about migration
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
+
+ consoleErrorSpy.mockRestore();
+ });
+
+ it('should preserve file content during migration', async () => {
+ const testContent = '{"entities": [{"name": "test", "type": "person"}]}';
+ await fs.writeFile(oldMemoryPath, testContent);
+
+ await ensureMemoryFilePath();
+
+ const migratedContent = await fs.readFile(newMemoryPath, 'utf-8');
+ expect(migratedContent).toBe(testContent);
+ });
+ });
+
+ describe('defaultMemoryPath', () => {
+ it('should end with memory.jsonl', () => {
+ expect(defaultMemoryPath).toMatch(/memory\.jsonl$/);
+ });
+
+ it('should be an absolute path', () => {
+ expect(path.isAbsolute(defaultMemoryPath)).toBe(true);
+ });
+ });
+});
diff --git a/src/memory/__tests__/knowledge-graph.test.ts b/src/memory/__tests__/knowledge-graph.test.ts
new file mode 100644
index 0000000000..a65d527b64
--- /dev/null
+++ b/src/memory/__tests__/knowledge-graph.test.ts
@@ -0,0 +1,394 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { promises as fs } from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { KnowledgeGraphManager, Entity, Relation, KnowledgeGraph } from '../index.js';
+
+describe('KnowledgeGraphManager', () => {
+ let manager: KnowledgeGraphManager;
+ let testFilePath: string;
+
+ beforeEach(async () => {
+ // Create a temporary test file path
+ testFilePath = path.join(
+ path.dirname(fileURLToPath(import.meta.url)),
+ `test-memory-${Date.now()}.jsonl`
+ );
+ manager = new KnowledgeGraphManager(testFilePath);
+ });
+
+ afterEach(async () => {
+ // Clean up test file
+ try {
+ await fs.unlink(testFilePath);
+ } catch (error) {
+ // Ignore errors if file doesn't exist
+ }
+ });
+
+ describe('createEntities', () => {
+ it('should create new entities', async () => {
+ const entities: Entity[] = [
+ { name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] },
+ { name: 'Bob', entityType: 'person', observations: ['likes programming'] },
+ ];
+
+ const newEntities = await manager.createEntities(entities);
+ expect(newEntities).toHaveLength(2);
+ expect(newEntities).toEqual(entities);
+
+ const graph = await manager.readGraph();
+ expect(graph.entities).toHaveLength(2);
+ });
+
+ it('should not create duplicate entities', async () => {
+ const entities: Entity[] = [
+ { name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] },
+ ];
+
+ await manager.createEntities(entities);
+ const newEntities = await manager.createEntities(entities);
+
+ expect(newEntities).toHaveLength(0);
+
+ const graph = await manager.readGraph();
+ expect(graph.entities).toHaveLength(1);
+ });
+
+ it('should handle empty entity arrays', async () => {
+ const newEntities = await manager.createEntities([]);
+ expect(newEntities).toHaveLength(0);
+ });
+ });
+
+ describe('createRelations', () => {
+ it('should create new relations', async () => {
+ await manager.createEntities([
+ { name: 'Alice', entityType: 'person', observations: [] },
+ { name: 'Bob', entityType: 'person', observations: [] },
+ ]);
+
+ const relations: Relation[] = [
+ { from: 'Alice', to: 'Bob', relationType: 'knows' },
+ ];
+
+ const newRelations = await manager.createRelations(relations);
+ expect(newRelations).toHaveLength(1);
+ expect(newRelations).toEqual(relations);
+
+ const graph = await manager.readGraph();
+ expect(graph.relations).toHaveLength(1);
+ });
+
+ it('should not create duplicate relations', async () => {
+ await manager.createEntities([
+ { name: 'Alice', entityType: 'person', observations: [] },
+ { name: 'Bob', entityType: 'person', observations: [] },
+ ]);
+
+ const relations: Relation[] = [
+ { from: 'Alice', to: 'Bob', relationType: 'knows' },
+ ];
+
+ await manager.createRelations(relations);
+ const newRelations = await manager.createRelations(relations);
+
+ expect(newRelations).toHaveLength(0);
+
+ const graph = await manager.readGraph();
+ expect(graph.relations).toHaveLength(1);
+ });
+
+ it('should handle empty relation arrays', async () => {
+ const newRelations = await manager.createRelations([]);
+ expect(newRelations).toHaveLength(0);
+ });
+ });
+
+ describe('addObservations', () => {
+ it('should add observations to existing entities', async () => {
+ await manager.createEntities([
+ { name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] },
+ ]);
+
+ const results = await manager.addObservations([
+ { entityName: 'Alice', contents: ['likes coffee', 'has a dog'] },
+ ]);
+
+ expect(results).toHaveLength(1);
+ expect(results[0].entityName).toBe('Alice');
+ expect(results[0].addedObservations).toHaveLength(2);
+
+ const graph = await manager.readGraph();
+ const alice = graph.entities.find(e => e.name === 'Alice');
+ expect(alice?.observations).toHaveLength(3);
+ });
+
+ it('should not add duplicate observations', async () => {
+ await manager.createEntities([
+ { name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] },
+ ]);
+
+ await manager.addObservations([
+ { entityName: 'Alice', contents: ['likes coffee'] },
+ ]);
+
+ const results = await manager.addObservations([
+ { entityName: 'Alice', contents: ['likes coffee', 'has a dog'] },
+ ]);
+
+ expect(results[0].addedObservations).toHaveLength(1);
+ expect(results[0].addedObservations).toContain('has a dog');
+
+ const graph = await manager.readGraph();
+ const alice = graph.entities.find(e => e.name === 'Alice');
+ expect(alice?.observations).toHaveLength(3);
+ });
+
+ it('should throw error for non-existent entity', async () => {
+ await expect(
+ manager.addObservations([
+ { entityName: 'NonExistent', contents: ['some observation'] },
+ ])
+ ).rejects.toThrow('Entity with name NonExistent not found');
+ });
+ });
+
+ describe('deleteEntities', () => {
+ it('should delete entities', async () => {
+ await manager.createEntities([
+ { name: 'Alice', entityType: 'person', observations: [] },
+ { name: 'Bob', entityType: 'person', observations: [] },
+ ]);
+
+ await manager.deleteEntities(['Alice']);
+
+ const graph = await manager.readGraph();
+ expect(graph.entities).toHaveLength(1);
+ expect(graph.entities[0].name).toBe('Bob');
+ });
+
+ it('should cascade delete relations when deleting entities', async () => {
+ await manager.createEntities([
+ { name: 'Alice', entityType: 'person', observations: [] },
+ { name: 'Bob', entityType: 'person', observations: [] },
+ { name: 'Charlie', entityType: 'person', observations: [] },
+ ]);
+
+ await manager.createRelations([
+ { from: 'Alice', to: 'Bob', relationType: 'knows' },
+ { from: 'Bob', to: 'Charlie', relationType: 'knows' },
+ ]);
+
+ await manager.deleteEntities(['Bob']);
+
+ const graph = await manager.readGraph();
+ expect(graph.entities).toHaveLength(2);
+ expect(graph.relations).toHaveLength(0);
+ });
+
+ it('should handle deleting non-existent entities', async () => {
+ await manager.deleteEntities(['NonExistent']);
+ const graph = await manager.readGraph();
+ expect(graph.entities).toHaveLength(0);
+ });
+ });
+
+ describe('deleteObservations', () => {
+ it('should delete observations from entities', async () => {
+ await manager.createEntities([
+ { name: 'Alice', entityType: 'person', observations: ['works at Acme Corp', 'likes coffee'] },
+ ]);
+
+ await manager.deleteObservations([
+ { entityName: 'Alice', observations: ['likes coffee'] },
+ ]);
+
+ const graph = await manager.readGraph();
+ const alice = graph.entities.find(e => e.name === 'Alice');
+ expect(alice?.observations).toHaveLength(1);
+ expect(alice?.observations).toContain('works at Acme Corp');
+ });
+
+ it('should handle deleting from non-existent entities', async () => {
+ await manager.deleteObservations([
+ { entityName: 'NonExistent', observations: ['some observation'] },
+ ]);
+ // Should not throw error
+ const graph = await manager.readGraph();
+ expect(graph.entities).toHaveLength(0);
+ });
+ });
+
+ describe('deleteRelations', () => {
+ it('should delete specific relations', async () => {
+ await manager.createEntities([
+ { name: 'Alice', entityType: 'person', observations: [] },
+ { name: 'Bob', entityType: 'person', observations: [] },
+ ]);
+
+ await manager.createRelations([
+ { from: 'Alice', to: 'Bob', relationType: 'knows' },
+ { from: 'Alice', to: 'Bob', relationType: 'works_with' },
+ ]);
+
+ await manager.deleteRelations([
+ { from: 'Alice', to: 'Bob', relationType: 'knows' },
+ ]);
+
+ const graph = await manager.readGraph();
+ expect(graph.relations).toHaveLength(1);
+ expect(graph.relations[0].relationType).toBe('works_with');
+ });
+ });
+
+ describe('readGraph', () => {
+ it('should return empty graph when file does not exist', async () => {
+ const graph = await manager.readGraph();
+ expect(graph.entities).toHaveLength(0);
+ expect(graph.relations).toHaveLength(0);
+ });
+
+ it('should return complete graph with entities and relations', async () => {
+ await manager.createEntities([
+ { name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] },
+ ]);
+
+ await manager.createRelations([
+ { from: 'Alice', to: 'Alice', relationType: 'self' },
+ ]);
+
+ const graph = await manager.readGraph();
+ expect(graph.entities).toHaveLength(1);
+ expect(graph.relations).toHaveLength(1);
+ });
+ });
+
+ describe('searchNodes', () => {
+ beforeEach(async () => {
+ await manager.createEntities([
+ { name: 'Alice', entityType: 'person', observations: ['works at Acme Corp', 'likes programming'] },
+ { name: 'Bob', entityType: 'person', observations: ['works at TechCo'] },
+ { name: 'Acme Corp', entityType: 'company', observations: ['tech company'] },
+ ]);
+
+ await manager.createRelations([
+ { from: 'Alice', to: 'Acme Corp', relationType: 'works_at' },
+ { from: 'Bob', to: 'Acme Corp', relationType: 'competitor' },
+ ]);
+ });
+
+ it('should search by entity name', async () => {
+ const result = await manager.searchNodes('Alice');
+ expect(result.entities).toHaveLength(1);
+ expect(result.entities[0].name).toBe('Alice');
+ });
+
+ it('should search by entity type', async () => {
+ const result = await manager.searchNodes('company');
+ expect(result.entities).toHaveLength(1);
+ expect(result.entities[0].name).toBe('Acme Corp');
+ });
+
+ it('should search by observation content', async () => {
+ const result = await manager.searchNodes('programming');
+ expect(result.entities).toHaveLength(1);
+ expect(result.entities[0].name).toBe('Alice');
+ });
+
+ it('should be case insensitive', async () => {
+ const result = await manager.searchNodes('ALICE');
+ expect(result.entities).toHaveLength(1);
+ expect(result.entities[0].name).toBe('Alice');
+ });
+
+ it('should include relations between matched entities', async () => {
+ const result = await manager.searchNodes('Acme');
+ expect(result.entities).toHaveLength(2); // Alice and Acme Corp
+ expect(result.relations).toHaveLength(1); // Only Alice -> Acme Corp relation
+ });
+
+ it('should return empty graph for no matches', async () => {
+ const result = await manager.searchNodes('NonExistent');
+ expect(result.entities).toHaveLength(0);
+ expect(result.relations).toHaveLength(0);
+ });
+ });
+
+ describe('openNodes', () => {
+ beforeEach(async () => {
+ await manager.createEntities([
+ { name: 'Alice', entityType: 'person', observations: [] },
+ { name: 'Bob', entityType: 'person', observations: [] },
+ { name: 'Charlie', entityType: 'person', observations: [] },
+ ]);
+
+ await manager.createRelations([
+ { from: 'Alice', to: 'Bob', relationType: 'knows' },
+ { from: 'Bob', to: 'Charlie', relationType: 'knows' },
+ ]);
+ });
+
+ it('should open specific nodes by name', async () => {
+ const result = await manager.openNodes(['Alice', 'Bob']);
+ expect(result.entities).toHaveLength(2);
+ expect(result.entities.map(e => e.name)).toContain('Alice');
+ expect(result.entities.map(e => e.name)).toContain('Bob');
+ });
+
+ it('should include relations between opened nodes', async () => {
+ const result = await manager.openNodes(['Alice', 'Bob']);
+ expect(result.relations).toHaveLength(1);
+ expect(result.relations[0].from).toBe('Alice');
+ expect(result.relations[0].to).toBe('Bob');
+ });
+
+ it('should exclude relations to unopened nodes', async () => {
+ const result = await manager.openNodes(['Bob']);
+ expect(result.relations).toHaveLength(0);
+ });
+
+ it('should handle opening non-existent nodes', async () => {
+ const result = await manager.openNodes(['NonExistent']);
+ expect(result.entities).toHaveLength(0);
+ });
+
+ it('should handle empty node list', async () => {
+ const result = await manager.openNodes([]);
+ expect(result.entities).toHaveLength(0);
+ expect(result.relations).toHaveLength(0);
+ });
+ });
+
+ describe('file persistence', () => {
+ it('should persist data across manager instances', async () => {
+ await manager.createEntities([
+ { name: 'Alice', entityType: 'person', observations: ['persistent data'] },
+ ]);
+
+ // Create new manager instance with same file path
+ const manager2 = new KnowledgeGraphManager(testFilePath);
+ const graph = await manager2.readGraph();
+
+ expect(graph.entities).toHaveLength(1);
+ expect(graph.entities[0].name).toBe('Alice');
+ });
+
+ it('should handle JSONL format correctly', async () => {
+ await manager.createEntities([
+ { name: 'Alice', entityType: 'person', observations: [] },
+ ]);
+ await manager.createRelations([
+ { from: 'Alice', to: 'Alice', relationType: 'self' },
+ ]);
+
+ // Read file directly
+ const fileContent = await fs.readFile(testFilePath, 'utf-8');
+ const lines = fileContent.split('\n').filter(line => line.trim());
+
+ expect(lines).toHaveLength(2);
+ expect(JSON.parse(lines[0])).toHaveProperty('type', 'entity');
+ expect(JSON.parse(lines[1])).toHaveProperty('type', 'relation');
+ });
+ });
+});
diff --git a/src/memory/index.ts b/src/memory/index.ts
index 204968f53c..94585a4481 100644
--- a/src/memory/index.ts
+++ b/src/memory/index.ts
@@ -11,10 +11,10 @@ import path from 'path';
import { fileURLToPath } from 'url';
// Define memory file path using environment variable with fallback
-const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.jsonl');
+export const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.jsonl');
// Handle backward compatibility: migrate memory.json to memory.jsonl if needed
-async function ensureMemoryFilePath(): Promise {
+export async function ensureMemoryFilePath(): Promise {
if (process.env.MEMORY_FILE_PATH) {
// Custom path provided, use it as-is (with absolute path resolution)
return path.isAbsolute(process.env.MEMORY_FILE_PATH)
@@ -50,28 +50,30 @@ async function ensureMemoryFilePath(): Promise {
let MEMORY_FILE_PATH: string;
// We are storing our memory using entities, relations, and observations in a graph structure
-interface Entity {
+export interface Entity {
name: string;
entityType: string;
observations: string[];
}
-interface Relation {
+export interface Relation {
from: string;
to: string;
relationType: string;
}
-interface KnowledgeGraph {
+export interface KnowledgeGraph {
entities: Entity[];
relations: Relation[];
}
// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
-class KnowledgeGraphManager {
+export class KnowledgeGraphManager {
+ constructor(private memoryFilePath: string) {}
+
private async loadGraph(): Promise {
try {
- const data = await fs.readFile(MEMORY_FILE_PATH, "utf-8");
+ const data = await fs.readFile(this.memoryFilePath, "utf-8");
const lines = data.split("\n").filter(line => line.trim() !== "");
return lines.reduce((graph: KnowledgeGraph, line) => {
const item = JSON.parse(line);
@@ -89,20 +91,20 @@ class KnowledgeGraphManager {
private async saveGraph(graph: KnowledgeGraph): Promise {
const lines = [
- ...graph.entities.map(e => JSON.stringify({
- type: "entity",
- name: e.name,
- entityType: e.entityType,
- observations: e.observations
+ ...graph.entities.map(e => JSON.stringify({
+ type: "entity",
+ name: e.name,
+ entityType: e.entityType,
+ observations: e.observations
})),
- ...graph.relations.map(r => JSON.stringify({
- type: "relation",
- from: r.from,
- to: r.to,
- relationType: r.relationType
+ ...graph.relations.map(r => JSON.stringify({
+ type: "relation",
+ from: r.from,
+ to: r.to,
+ relationType: r.relationType
})),
];
- await fs.writeFile(MEMORY_FILE_PATH, lines.join("\n"));
+ await fs.writeFile(this.memoryFilePath, lines.join("\n"));
}
async createEntities(entities: Entity[]): Promise {
@@ -222,7 +224,7 @@ class KnowledgeGraphManager {
}
}
-const knowledgeGraphManager = new KnowledgeGraphManager();
+let knowledgeGraphManager: KnowledgeGraphManager;
// The server instance and tools exposed to Claude
@@ -465,7 +467,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
async function main() {
// Initialize memory file path with backward compatibility
MEMORY_FILE_PATH = await ensureMemoryFilePath();
-
+
+ // Initialize knowledge graph manager with the memory file path
+ knowledgeGraphManager = new KnowledgeGraphManager(MEMORY_FILE_PATH);
+
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Knowledge Graph MCP Server running on stdio");
diff --git a/src/memory/package.json b/src/memory/package.json
index bb133ce218..3af1c44d01 100644
--- a/src/memory/package.json
+++ b/src/memory/package.json
@@ -16,14 +16,17 @@
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
- "watch": "tsc --watch"
+ "watch": "tsc --watch",
+ "test": "vitest run --coverage"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.19.1"
},
"devDependencies": {
"@types/node": "^22",
+ "@vitest/coverage-v8": "^2.1.8",
"shx": "^0.3.4",
- "typescript": "^5.6.2"
+ "typescript": "^5.6.2",
+ "vitest": "^2.1.8"
}
}
\ No newline at end of file
diff --git a/src/memory/vitest.config.ts b/src/memory/vitest.config.ts
new file mode 100644
index 0000000000..d414ec8f52
--- /dev/null
+++ b/src/memory/vitest.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: 'node',
+ include: ['**/__tests__/**/*.test.ts'],
+ coverage: {
+ provider: 'v8',
+ include: ['**/*.ts'],
+ exclude: ['**/__tests__/**', '**/dist/**'],
+ },
+ },
+});