@synth-coder/memhub 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +45 -45
- package/.factory/commands/opsx-apply.md +150 -0
- package/.factory/commands/opsx-archive.md +155 -0
- package/.factory/commands/opsx-explore.md +171 -0
- package/.factory/commands/opsx-propose.md +104 -0
- package/.factory/skills/openspec-apply-change/SKILL.md +156 -0
- package/.factory/skills/openspec-archive-change/SKILL.md +114 -0
- package/.factory/skills/openspec-explore/SKILL.md +288 -0
- package/.factory/skills/openspec-propose/SKILL.md +110 -0
- package/.github/workflows/ci.yml +74 -74
- package/.iflow/commands/opsx-apply.md +152 -152
- package/.iflow/commands/opsx-archive.md +157 -157
- package/.iflow/commands/opsx-explore.md +173 -173
- package/.iflow/commands/opsx-propose.md +106 -106
- package/.iflow/skills/openspec-apply-change/SKILL.md +156 -156
- package/.iflow/skills/openspec-archive-change/SKILL.md +114 -114
- package/.iflow/skills/openspec-explore/SKILL.md +288 -288
- package/.iflow/skills/openspec-propose/SKILL.md +110 -110
- package/.prettierrc +11 -11
- package/AGENTS.md +169 -26
- package/README.md +195 -195
- package/README.zh-CN.md +193 -193
- package/dist/src/contracts/mcp.js +34 -34
- package/dist/src/server/mcp-server.d.ts +8 -0
- package/dist/src/server/mcp-server.d.ts.map +1 -1
- package/dist/src/server/mcp-server.js +23 -2
- package/dist/src/server/mcp-server.js.map +1 -1
- package/dist/src/services/memory-service.d.ts +1 -0
- package/dist/src/services/memory-service.d.ts.map +1 -1
- package/dist/src/services/memory-service.js +125 -82
- package/dist/src/services/memory-service.js.map +1 -1
- package/docs/architecture-diagrams.md +368 -0
- package/docs/architecture.md +381 -349
- package/docs/contracts.md +190 -119
- package/docs/prompt-template.md +33 -79
- package/docs/proposals/mcp-typescript-sdk-refactor.md +568 -568
- package/docs/proposals/proposal-close-gates.md +58 -58
- package/docs/tool-calling-policy.md +101 -107
- package/docs/vector-search.md +306 -0
- package/package.json +59 -58
- package/src/contracts/index.ts +12 -12
- package/src/contracts/mcp.ts +222 -222
- package/src/contracts/schemas.ts +307 -307
- package/src/contracts/types.ts +410 -410
- package/src/index.ts +8 -8
- package/src/server/index.ts +5 -5
- package/src/server/mcp-server.ts +185 -161
- package/src/services/embedding-service.ts +114 -114
- package/src/services/index.ts +5 -5
- package/src/services/memory-service.ts +663 -621
- package/src/storage/frontmatter-parser.ts +243 -243
- package/src/storage/index.ts +6 -6
- package/src/storage/markdown-storage.ts +236 -236
- package/src/storage/vector-index.ts +160 -160
- package/src/utils/index.ts +5 -5
- package/src/utils/slugify.ts +63 -63
- package/test/contracts/schemas.test.ts +313 -313
- package/test/contracts/types.test.ts +21 -21
- package/test/frontmatter-parser-more.test.ts +94 -94
- package/test/server/mcp-server.test.ts +210 -169
- package/test/services/memory-service-edge.test.ts +248 -248
- package/test/services/memory-service.test.ts +278 -278
- package/test/storage/frontmatter-parser.test.ts +222 -222
- package/test/storage/markdown-storage.test.ts +216 -216
- package/test/storage/storage-edge.test.ts +238 -238
- package/test/storage/vector-index.test.ts +153 -153
- package/test/utils/slugify-edge.test.ts +94 -94
- package/test/utils/slugify.test.ts +68 -68
- package/tsconfig.json +25 -25
- package/tsconfig.test.json +8 -8
- package/vitest.config.ts +29 -29
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Additional FrontMatter Parser Tests
|
|
3
|
-
* For better branch coverage
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect } from 'vitest';
|
|
7
|
-
import {
|
|
8
|
-
parseFrontMatter,
|
|
9
|
-
stringifyFrontMatter,
|
|
10
|
-
FrontMatterError,
|
|
11
|
-
} from '../src/storage/frontmatter-parser.js';
|
|
12
|
-
import type { MemoryFrontMatter } from '../src/contracts/types.js';
|
|
13
|
-
|
|
14
|
-
describe('parseFrontMatter additional branches', () => {
|
|
15
|
-
it('should throw for invalid front matter format', () => {
|
|
16
|
-
// Missing closing ---
|
|
17
|
-
const markdown = '---\nid: "test"\n# Title\n\nContent';
|
|
18
|
-
expect(() => parseFrontMatter(markdown)).toThrow(FrontMatterError);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('should parse with Windows line endings', () => {
|
|
22
|
-
const markdown = `---\r\nid: "550e8400-e29b-41d4-a716-446655440000"\r\ncreated_at: "2024-03-15T10:30:00Z"\r\nupdated_at: "2024-03-15T10:30:00Z"\r\ntags: []\r\ncategory: "general"\r\nimportance: 3\r\n---\r\n\r\n# Title\r\n\r\nContent`;
|
|
23
|
-
|
|
24
|
-
const result = parseFrontMatter(markdown);
|
|
25
|
-
expect(result.frontMatter.id).toBe('550e8400-e29b-41d4-a716-446655440000');
|
|
26
|
-
expect(result.title).toBe('Title');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should handle front matter with extra whitespace', () => {
|
|
30
|
-
const markdown = `---
|
|
31
|
-
id: "550e8400-e29b-41d4-a716-446655440000"
|
|
32
|
-
created_at: "2024-03-15T10:30:00Z"
|
|
33
|
-
updated_at: "2024-03-15T10:30:00Z"
|
|
34
|
-
tags: []
|
|
35
|
-
category: "general"
|
|
36
|
-
importance: 3
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
# Title
|
|
40
|
-
|
|
41
|
-
Content`;
|
|
42
|
-
|
|
43
|
-
const result = parseFrontMatter(markdown);
|
|
44
|
-
expect(result.frontMatter.id).toBe('550e8400-e29b-41d4-a716-446655440000');
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('stringifyFrontMatter additional branches', () => {
|
|
49
|
-
const baseFrontMatter: MemoryFrontMatter = {
|
|
50
|
-
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
51
|
-
created_at: '2024-03-15T10:30:00Z',
|
|
52
|
-
updated_at: '2024-03-15T10:30:00Z',
|
|
53
|
-
tags: [],
|
|
54
|
-
category: 'general',
|
|
55
|
-
importance: 3,
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
it('should handle empty title', () => {
|
|
59
|
-
const result = stringifyFrontMatter(baseFrontMatter, '', 'Content');
|
|
60
|
-
expect(result).toContain('---');
|
|
61
|
-
expect(result).toContain('Content');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should handle empty content', () => {
|
|
65
|
-
const result = stringifyFrontMatter(baseFrontMatter, 'Title', '');
|
|
66
|
-
expect(result).toContain('# Title');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should handle both empty title and content', () => {
|
|
70
|
-
const result = stringifyFrontMatter(baseFrontMatter, '', '');
|
|
71
|
-
expect(result).toContain('---');
|
|
72
|
-
expect(result).toContain('id:');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should handle single tag', () => {
|
|
76
|
-
const fm = { ...baseFrontMatter, tags: ['single'] };
|
|
77
|
-
const result = stringifyFrontMatter(fm, 'Title', 'Content');
|
|
78
|
-
expect(result).toContain('single');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should handle many tags', () => {
|
|
82
|
-
const fm = { ...baseFrontMatter, tags: ['a', 'b', 'c', 'd', 'e'] };
|
|
83
|
-
const result = stringifyFrontMatter(fm, 'Title', 'Content');
|
|
84
|
-
expect(result).toContain('a');
|
|
85
|
-
expect(result).toContain('e');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('should end with single newline', () => {
|
|
89
|
-
const result = stringifyFrontMatter(baseFrontMatter, 'Title', 'Content');
|
|
90
|
-
const lines = result.split('\n');
|
|
91
|
-
expect(lines[lines.length - 1]).toBe('');
|
|
92
|
-
expect(lines[lines.length - 2]).not.toBe('');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Additional FrontMatter Parser Tests
|
|
3
|
+
* For better branch coverage
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
parseFrontMatter,
|
|
9
|
+
stringifyFrontMatter,
|
|
10
|
+
FrontMatterError,
|
|
11
|
+
} from '../src/storage/frontmatter-parser.js';
|
|
12
|
+
import type { MemoryFrontMatter } from '../src/contracts/types.js';
|
|
13
|
+
|
|
14
|
+
describe('parseFrontMatter additional branches', () => {
|
|
15
|
+
it('should throw for invalid front matter format', () => {
|
|
16
|
+
// Missing closing ---
|
|
17
|
+
const markdown = '---\nid: "test"\n# Title\n\nContent';
|
|
18
|
+
expect(() => parseFrontMatter(markdown)).toThrow(FrontMatterError);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should parse with Windows line endings', () => {
|
|
22
|
+
const markdown = `---\r\nid: "550e8400-e29b-41d4-a716-446655440000"\r\ncreated_at: "2024-03-15T10:30:00Z"\r\nupdated_at: "2024-03-15T10:30:00Z"\r\ntags: []\r\ncategory: "general"\r\nimportance: 3\r\n---\r\n\r\n# Title\r\n\r\nContent`;
|
|
23
|
+
|
|
24
|
+
const result = parseFrontMatter(markdown);
|
|
25
|
+
expect(result.frontMatter.id).toBe('550e8400-e29b-41d4-a716-446655440000');
|
|
26
|
+
expect(result.title).toBe('Title');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should handle front matter with extra whitespace', () => {
|
|
30
|
+
const markdown = `---
|
|
31
|
+
id: "550e8400-e29b-41d4-a716-446655440000"
|
|
32
|
+
created_at: "2024-03-15T10:30:00Z"
|
|
33
|
+
updated_at: "2024-03-15T10:30:00Z"
|
|
34
|
+
tags: []
|
|
35
|
+
category: "general"
|
|
36
|
+
importance: 3
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# Title
|
|
40
|
+
|
|
41
|
+
Content`;
|
|
42
|
+
|
|
43
|
+
const result = parseFrontMatter(markdown);
|
|
44
|
+
expect(result.frontMatter.id).toBe('550e8400-e29b-41d4-a716-446655440000');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('stringifyFrontMatter additional branches', () => {
|
|
49
|
+
const baseFrontMatter: MemoryFrontMatter = {
|
|
50
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
51
|
+
created_at: '2024-03-15T10:30:00Z',
|
|
52
|
+
updated_at: '2024-03-15T10:30:00Z',
|
|
53
|
+
tags: [],
|
|
54
|
+
category: 'general',
|
|
55
|
+
importance: 3,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
it('should handle empty title', () => {
|
|
59
|
+
const result = stringifyFrontMatter(baseFrontMatter, '', 'Content');
|
|
60
|
+
expect(result).toContain('---');
|
|
61
|
+
expect(result).toContain('Content');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should handle empty content', () => {
|
|
65
|
+
const result = stringifyFrontMatter(baseFrontMatter, 'Title', '');
|
|
66
|
+
expect(result).toContain('# Title');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle both empty title and content', () => {
|
|
70
|
+
const result = stringifyFrontMatter(baseFrontMatter, '', '');
|
|
71
|
+
expect(result).toContain('---');
|
|
72
|
+
expect(result).toContain('id:');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should handle single tag', () => {
|
|
76
|
+
const fm = { ...baseFrontMatter, tags: ['single'] };
|
|
77
|
+
const result = stringifyFrontMatter(fm, 'Title', 'Content');
|
|
78
|
+
expect(result).toContain('single');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle many tags', () => {
|
|
82
|
+
const fm = { ...baseFrontMatter, tags: ['a', 'b', 'c', 'd', 'e'] };
|
|
83
|
+
const result = stringifyFrontMatter(fm, 'Title', 'Content');
|
|
84
|
+
expect(result).toContain('a');
|
|
85
|
+
expect(result).toContain('e');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should end with single newline', () => {
|
|
89
|
+
const result = stringifyFrontMatter(baseFrontMatter, 'Title', 'Content');
|
|
90
|
+
const lines = result.split('\n');
|
|
91
|
+
expect(lines[lines.length - 1]).toBe('');
|
|
92
|
+
expect(lines[lines.length - 2]).not.toBe('');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -1,169 +1,210 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Server Tests
|
|
3
|
-
* Tests for the MCP Server using @modelcontextprotocol/sdk
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
-
import { mkdtempSync, rmSync } from 'fs';
|
|
8
|
-
import { tmpdir } from 'os';
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
import { createMcpServer } from '../../src/server/mcp-server.js';
|
|
11
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
12
|
-
import {
|
|
13
|
-
TOOL_DEFINITIONS,
|
|
14
|
-
SERVER_INFO,
|
|
15
|
-
ERROR_CODES,
|
|
16
|
-
MCP_PROTOCOL_VERSION,
|
|
17
|
-
} from '../../src/contracts/mcp.js';
|
|
18
|
-
import { MemoryService } from '../../src/services/memory-service.js';
|
|
19
|
-
import { MemoryLoadInputSchema, MemoryUpdateInputV2Schema } from '../../src/contracts/schemas.js';
|
|
20
|
-
|
|
21
|
-
describe('McpServer (SDK)', () => {
|
|
22
|
-
let tempDir: string;
|
|
23
|
-
let server: Server;
|
|
24
|
-
let memoryService: MemoryService;
|
|
25
|
-
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
tempDir = mkdtempSync(join(tmpdir(), 'memhub-server-test-'));
|
|
28
|
-
process.env.MEMHUB_STORAGE_PATH = tempDir;
|
|
29
|
-
process.env.MEMHUB_VECTOR_SEARCH = 'false';
|
|
30
|
-
server = createMcpServer();
|
|
31
|
-
memoryService = new MemoryService({ storagePath: tempDir, vectorSearch: false });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
36
|
-
delete process.env.MEMHUB_STORAGE_PATH;
|
|
37
|
-
delete process.env.MEMHUB_VECTOR_SEARCH;
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('createMcpServer', () => {
|
|
41
|
-
it('should create server instance', () => {
|
|
42
|
-
expect(server).toBeDefined();
|
|
43
|
-
expect(server).toBeInstanceOf(Server);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe('TOOL_DEFINITIONS', () => {
|
|
48
|
-
it('should have 2 tools defined', () => {
|
|
49
|
-
expect(TOOL_DEFINITIONS).toHaveLength(2);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should include STM-first tools', () => {
|
|
53
|
-
const toolNames = TOOL_DEFINITIONS.map(t => t.name);
|
|
54
|
-
expect(toolNames).toContain('memory_load');
|
|
55
|
-
expect(toolNames).toContain('memory_update');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should have descriptions for all tools', () => {
|
|
59
|
-
for (const tool of TOOL_DEFINITIONS) {
|
|
60
|
-
expect(tool.description).toBeDefined();
|
|
61
|
-
expect(tool.description.length).toBeGreaterThan(0);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should have input schemas for all tools', () => {
|
|
66
|
-
for (const tool of TOOL_DEFINITIONS) {
|
|
67
|
-
expect(tool.inputSchema).toBeDefined();
|
|
68
|
-
expect(tool.inputSchema.type).toBe('object');
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('ServerInfo', () => {
|
|
74
|
-
it('should have correct server name', () => {
|
|
75
|
-
expect(SERVER_INFO.name).toBe('memhub');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should have version', () => {
|
|
79
|
-
expect(SERVER_INFO.version).toBeDefined();
|
|
80
|
-
expect(SERVER_INFO.version).toMatch(/^\d+\.\d+\.\d+/);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('ErrorCodes', () => {
|
|
85
|
-
it('should have standard MCP error codes', () => {
|
|
86
|
-
expect(ERROR_CODES.PARSE_ERROR).toBe(-32700);
|
|
87
|
-
expect(ERROR_CODES.INVALID_REQUEST).toBe(-32600);
|
|
88
|
-
expect(ERROR_CODES.METHOD_NOT_FOUND).toBe(-32601);
|
|
89
|
-
expect(ERROR_CODES.INVALID_PARAMS).toBe(-32602);
|
|
90
|
-
expect(ERROR_CODES.INTERNAL_ERROR).toBe(-32603);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should have custom MemHub error codes', () => {
|
|
94
|
-
expect(ERROR_CODES.NOT_FOUND).toBe(-32001);
|
|
95
|
-
expect(ERROR_CODES.STORAGE_ERROR).toBe(-32002);
|
|
96
|
-
expect(ERROR_CODES.VALIDATION_ERROR).toBe(-32003);
|
|
97
|
-
expect(ERROR_CODES.DUPLICATE_ERROR).toBe(-32004);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe('Protocol', () => {
|
|
102
|
-
it('should have correct protocol version', () => {
|
|
103
|
-
expect(MCP_PROTOCOL_VERSION).toBe('2024-11-05');
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('Tool Integration Tests', () => {
|
|
108
|
-
it('should handle memory_update via MemoryService', async () => {
|
|
109
|
-
const input = MemoryUpdateInputV2Schema.parse({
|
|
110
|
-
sessionId: '550e8400-e29b-41d4-a716-446655440000',
|
|
111
|
-
entryType: 'decision',
|
|
112
|
-
title: 'Test decision',
|
|
113
|
-
content: 'This is a test decision',
|
|
114
|
-
tags: ['test'],
|
|
115
|
-
category: 'general',
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const result = await memoryService.memoryUpdate(input);
|
|
119
|
-
|
|
120
|
-
expect(result).toHaveProperty('id');
|
|
121
|
-
expect(result).toHaveProperty('sessionId');
|
|
122
|
-
expect(result.sessionId).toBe('550e8400-e29b-41d4-a716-446655440000');
|
|
123
|
-
expect(result.created).toBe(true);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should handle memory_load via MemoryService', async () => {
|
|
127
|
-
// First create a memory
|
|
128
|
-
const updateInput = MemoryUpdateInputV2Schema.parse({
|
|
129
|
-
sessionId: '550e8400-e29b-41d4-a716-446655440001',
|
|
130
|
-
entryType: 'preference',
|
|
131
|
-
title: 'Test preference',
|
|
132
|
-
content: 'I prefer chocolate ice cream',
|
|
133
|
-
tags: ['food', 'preference'],
|
|
134
|
-
category: 'personal',
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
const updateResult = await memoryService.memoryUpdate(updateInput);
|
|
138
|
-
|
|
139
|
-
// Then load it
|
|
140
|
-
const loadInput = MemoryLoadInputSchema.parse({
|
|
141
|
-
id: updateResult.id,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const loadResult = await memoryService.memoryLoad(loadInput);
|
|
145
|
-
|
|
146
|
-
expect(loadResult).toHaveProperty('items');
|
|
147
|
-
expect(loadResult.items.length).toBeGreaterThan(0);
|
|
148
|
-
expect(loadResult.items[0].title).toBe('Test preference');
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('should return error for invalid tool arguments', () => {
|
|
152
|
-
expect(() => {
|
|
153
|
-
MemoryUpdateInputV2Schema.parse({ title: '' }); // content is required
|
|
154
|
-
}).toThrow();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should validate memory_load input schema', () => {
|
|
158
|
-
const validInput = MemoryLoadInputSchema.parse({
|
|
159
|
-
query: 'test query',
|
|
160
|
-
limit: 10,
|
|
161
|
-
category: 'general',
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
expect(validInput.query).toBe('test query');
|
|
165
|
-
expect(validInput.limit).toBe(10);
|
|
166
|
-
expect(validInput.category).toBe('general');
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server Tests
|
|
3
|
+
* Tests for the MCP Server using @modelcontextprotocol/sdk
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import { mkdtempSync, rmSync } from 'fs';
|
|
8
|
+
import { tmpdir, homedir } from 'os';
|
|
9
|
+
import { join, resolve } from 'path';
|
|
10
|
+
import { createMcpServer, resolveStoragePath } from '../../src/server/mcp-server.js';
|
|
11
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
12
|
+
import {
|
|
13
|
+
TOOL_DEFINITIONS,
|
|
14
|
+
SERVER_INFO,
|
|
15
|
+
ERROR_CODES,
|
|
16
|
+
MCP_PROTOCOL_VERSION,
|
|
17
|
+
} from '../../src/contracts/mcp.js';
|
|
18
|
+
import { MemoryService } from '../../src/services/memory-service.js';
|
|
19
|
+
import { MemoryLoadInputSchema, MemoryUpdateInputV2Schema } from '../../src/contracts/schemas.js';
|
|
20
|
+
|
|
21
|
+
describe('McpServer (SDK)', () => {
|
|
22
|
+
let tempDir: string;
|
|
23
|
+
let server: Server;
|
|
24
|
+
let memoryService: MemoryService;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
tempDir = mkdtempSync(join(tmpdir(), 'memhub-server-test-'));
|
|
28
|
+
process.env.MEMHUB_STORAGE_PATH = tempDir;
|
|
29
|
+
process.env.MEMHUB_VECTOR_SEARCH = 'false';
|
|
30
|
+
server = createMcpServer();
|
|
31
|
+
memoryService = new MemoryService({ storagePath: tempDir, vectorSearch: false });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
36
|
+
delete process.env.MEMHUB_STORAGE_PATH;
|
|
37
|
+
delete process.env.MEMHUB_VECTOR_SEARCH;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('createMcpServer', () => {
|
|
41
|
+
it('should create server instance', () => {
|
|
42
|
+
expect(server).toBeDefined();
|
|
43
|
+
expect(server).toBeInstanceOf(Server);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('TOOL_DEFINITIONS', () => {
|
|
48
|
+
it('should have 2 tools defined', () => {
|
|
49
|
+
expect(TOOL_DEFINITIONS).toHaveLength(2);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should include STM-first tools', () => {
|
|
53
|
+
const toolNames = TOOL_DEFINITIONS.map(t => t.name);
|
|
54
|
+
expect(toolNames).toContain('memory_load');
|
|
55
|
+
expect(toolNames).toContain('memory_update');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should have descriptions for all tools', () => {
|
|
59
|
+
for (const tool of TOOL_DEFINITIONS) {
|
|
60
|
+
expect(tool.description).toBeDefined();
|
|
61
|
+
expect(tool.description.length).toBeGreaterThan(0);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should have input schemas for all tools', () => {
|
|
66
|
+
for (const tool of TOOL_DEFINITIONS) {
|
|
67
|
+
expect(tool.inputSchema).toBeDefined();
|
|
68
|
+
expect(tool.inputSchema.type).toBe('object');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('ServerInfo', () => {
|
|
74
|
+
it('should have correct server name', () => {
|
|
75
|
+
expect(SERVER_INFO.name).toBe('memhub');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should have version', () => {
|
|
79
|
+
expect(SERVER_INFO.version).toBeDefined();
|
|
80
|
+
expect(SERVER_INFO.version).toMatch(/^\d+\.\d+\.\d+/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('ErrorCodes', () => {
|
|
85
|
+
it('should have standard MCP error codes', () => {
|
|
86
|
+
expect(ERROR_CODES.PARSE_ERROR).toBe(-32700);
|
|
87
|
+
expect(ERROR_CODES.INVALID_REQUEST).toBe(-32600);
|
|
88
|
+
expect(ERROR_CODES.METHOD_NOT_FOUND).toBe(-32601);
|
|
89
|
+
expect(ERROR_CODES.INVALID_PARAMS).toBe(-32602);
|
|
90
|
+
expect(ERROR_CODES.INTERNAL_ERROR).toBe(-32603);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should have custom MemHub error codes', () => {
|
|
94
|
+
expect(ERROR_CODES.NOT_FOUND).toBe(-32001);
|
|
95
|
+
expect(ERROR_CODES.STORAGE_ERROR).toBe(-32002);
|
|
96
|
+
expect(ERROR_CODES.VALIDATION_ERROR).toBe(-32003);
|
|
97
|
+
expect(ERROR_CODES.DUPLICATE_ERROR).toBe(-32004);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('Protocol', () => {
|
|
102
|
+
it('should have correct protocol version', () => {
|
|
103
|
+
expect(MCP_PROTOCOL_VERSION).toBe('2024-11-05');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('Tool Integration Tests', () => {
|
|
108
|
+
it('should handle memory_update via MemoryService', async () => {
|
|
109
|
+
const input = MemoryUpdateInputV2Schema.parse({
|
|
110
|
+
sessionId: '550e8400-e29b-41d4-a716-446655440000',
|
|
111
|
+
entryType: 'decision',
|
|
112
|
+
title: 'Test decision',
|
|
113
|
+
content: 'This is a test decision',
|
|
114
|
+
tags: ['test'],
|
|
115
|
+
category: 'general',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const result = await memoryService.memoryUpdate(input);
|
|
119
|
+
|
|
120
|
+
expect(result).toHaveProperty('id');
|
|
121
|
+
expect(result).toHaveProperty('sessionId');
|
|
122
|
+
expect(result.sessionId).toBe('550e8400-e29b-41d4-a716-446655440000');
|
|
123
|
+
expect(result.created).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should handle memory_load via MemoryService', async () => {
|
|
127
|
+
// First create a memory
|
|
128
|
+
const updateInput = MemoryUpdateInputV2Schema.parse({
|
|
129
|
+
sessionId: '550e8400-e29b-41d4-a716-446655440001',
|
|
130
|
+
entryType: 'preference',
|
|
131
|
+
title: 'Test preference',
|
|
132
|
+
content: 'I prefer chocolate ice cream',
|
|
133
|
+
tags: ['food', 'preference'],
|
|
134
|
+
category: 'personal',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const updateResult = await memoryService.memoryUpdate(updateInput);
|
|
138
|
+
|
|
139
|
+
// Then load it
|
|
140
|
+
const loadInput = MemoryLoadInputSchema.parse({
|
|
141
|
+
id: updateResult.id,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const loadResult = await memoryService.memoryLoad(loadInput);
|
|
145
|
+
|
|
146
|
+
expect(loadResult).toHaveProperty('items');
|
|
147
|
+
expect(loadResult.items.length).toBeGreaterThan(0);
|
|
148
|
+
expect(loadResult.items[0].title).toBe('Test preference');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return error for invalid tool arguments', () => {
|
|
152
|
+
expect(() => {
|
|
153
|
+
MemoryUpdateInputV2Schema.parse({ title: '' }); // content is required
|
|
154
|
+
}).toThrow();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should validate memory_load input schema', () => {
|
|
158
|
+
const validInput = MemoryLoadInputSchema.parse({
|
|
159
|
+
query: 'test query',
|
|
160
|
+
limit: 10,
|
|
161
|
+
category: 'general',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(validInput.query).toBe('test query');
|
|
165
|
+
expect(validInput.limit).toBe(10);
|
|
166
|
+
expect(validInput.category).toBe('general');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('resolveStoragePath', () => {
|
|
171
|
+
const originalEnv = process.env.MEMHUB_STORAGE_PATH;
|
|
172
|
+
const originalCwd = process.cwd.bind(process);
|
|
173
|
+
|
|
174
|
+
afterEach(() => {
|
|
175
|
+
if (originalEnv === undefined) {
|
|
176
|
+
delete process.env.MEMHUB_STORAGE_PATH;
|
|
177
|
+
} else {
|
|
178
|
+
process.env.MEMHUB_STORAGE_PATH = originalEnv;
|
|
179
|
+
}
|
|
180
|
+
process.cwd = originalCwd;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should return ~/.memhub by default', () => {
|
|
184
|
+
delete process.env.MEMHUB_STORAGE_PATH;
|
|
185
|
+
const expectedPath = join(homedir(), '.memhub');
|
|
186
|
+
expect(resolveStoragePath()).toBe(expectedPath);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should use absolute path from MEMHUB_STORAGE_PATH', () => {
|
|
190
|
+
const absolutePath = process.platform === 'win32' ? 'C:\\custom\\path' : '/custom/path';
|
|
191
|
+
process.env.MEMHUB_STORAGE_PATH = absolutePath;
|
|
192
|
+
expect(resolveStoragePath()).toBe(absolutePath);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should resolve relative path from cwd', () => {
|
|
196
|
+
const cwd = process.platform === 'win32' ? 'C:\\project' : '/project';
|
|
197
|
+
process.cwd = () => cwd;
|
|
198
|
+
process.env.MEMHUB_STORAGE_PATH = '.memhub';
|
|
199
|
+
expect(resolveStoragePath()).toBe(resolve(cwd, '.memhub'));
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should resolve .memhub relative path', () => {
|
|
203
|
+
const cwd = process.platform === 'win32' ? 'C:\\myproject' : '/myproject';
|
|
204
|
+
process.cwd = () => cwd;
|
|
205
|
+
process.env.MEMHUB_STORAGE_PATH = '.memhub';
|
|
206
|
+
const expected = process.platform === 'win32' ? 'C:\\myproject\\.memhub' : '/myproject/.memhub';
|
|
207
|
+
expect(resolveStoragePath()).toBe(expected);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|