@synth-coder/memhub 0.2.3 → 0.2.4

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.
Files changed (143) hide show
  1. package/.eslintrc.cjs +45 -45
  2. package/.factory/commands/opsx-apply.md +150 -150
  3. package/.factory/commands/opsx-archive.md +155 -155
  4. package/.factory/commands/opsx-explore.md +171 -171
  5. package/.factory/commands/opsx-propose.md +104 -104
  6. package/.factory/skills/openspec-apply-change/SKILL.md +156 -156
  7. package/.factory/skills/openspec-archive-change/SKILL.md +114 -114
  8. package/.factory/skills/openspec-explore/SKILL.md +288 -288
  9. package/.factory/skills/openspec-propose/SKILL.md +110 -110
  10. package/.github/workflows/ci.yml +110 -74
  11. package/.github/workflows/release.yml +67 -0
  12. package/.iflow/commands/opsx-apply.md +152 -152
  13. package/.iflow/commands/opsx-archive.md +157 -157
  14. package/.iflow/commands/opsx-explore.md +173 -173
  15. package/.iflow/commands/opsx-propose.md +106 -106
  16. package/.iflow/skills/openspec-apply-change/SKILL.md +156 -156
  17. package/.iflow/skills/openspec-archive-change/SKILL.md +114 -114
  18. package/.iflow/skills/openspec-explore/SKILL.md +288 -288
  19. package/.iflow/skills/openspec-propose/SKILL.md +110 -110
  20. package/.prettierrc +11 -11
  21. package/AGENTS.md +167 -169
  22. package/README.md +276 -195
  23. package/README.zh-CN.md +245 -193
  24. package/dist/src/cli/agents/claude-code.d.ts +5 -0
  25. package/dist/src/cli/agents/claude-code.d.ts.map +1 -0
  26. package/dist/src/cli/agents/claude-code.js +14 -0
  27. package/dist/src/cli/agents/claude-code.js.map +1 -0
  28. package/dist/src/cli/agents/cline.d.ts +5 -0
  29. package/dist/src/cli/agents/cline.d.ts.map +1 -0
  30. package/dist/src/cli/agents/cline.js +14 -0
  31. package/dist/src/cli/agents/cline.js.map +1 -0
  32. package/dist/src/cli/agents/codex.d.ts +5 -0
  33. package/dist/src/cli/agents/codex.d.ts.map +1 -0
  34. package/dist/src/cli/agents/codex.js +14 -0
  35. package/dist/src/cli/agents/codex.js.map +1 -0
  36. package/dist/src/cli/agents/cursor.d.ts +5 -0
  37. package/dist/src/cli/agents/cursor.d.ts.map +1 -0
  38. package/dist/src/cli/agents/cursor.js +14 -0
  39. package/dist/src/cli/agents/cursor.js.map +1 -0
  40. package/dist/src/cli/agents/factory-droid.d.ts +5 -0
  41. package/dist/src/cli/agents/factory-droid.d.ts.map +1 -0
  42. package/dist/src/cli/agents/factory-droid.js +14 -0
  43. package/dist/src/cli/agents/factory-droid.js.map +1 -0
  44. package/dist/src/cli/agents/gemini-cli.d.ts +5 -0
  45. package/dist/src/cli/agents/gemini-cli.d.ts.map +1 -0
  46. package/dist/src/cli/agents/gemini-cli.js +14 -0
  47. package/dist/src/cli/agents/gemini-cli.js.map +1 -0
  48. package/dist/src/cli/agents/index.d.ts +14 -0
  49. package/dist/src/cli/agents/index.d.ts.map +1 -0
  50. package/dist/src/cli/agents/index.js +30 -0
  51. package/dist/src/cli/agents/index.js.map +1 -0
  52. package/dist/src/cli/agents/windsurf.d.ts +5 -0
  53. package/dist/src/cli/agents/windsurf.d.ts.map +1 -0
  54. package/dist/src/cli/agents/windsurf.js +14 -0
  55. package/dist/src/cli/agents/windsurf.js.map +1 -0
  56. package/dist/src/cli/index.d.ts +8 -0
  57. package/dist/src/cli/index.d.ts.map +1 -0
  58. package/dist/src/cli/index.js +168 -0
  59. package/dist/src/cli/index.js.map +1 -0
  60. package/dist/src/cli/init.d.ts +34 -0
  61. package/dist/src/cli/init.d.ts.map +1 -0
  62. package/dist/src/cli/init.js +160 -0
  63. package/dist/src/cli/init.js.map +1 -0
  64. package/dist/src/cli/instructions.d.ts +29 -0
  65. package/dist/src/cli/instructions.d.ts.map +1 -0
  66. package/dist/src/cli/instructions.js +141 -0
  67. package/dist/src/cli/instructions.js.map +1 -0
  68. package/dist/src/cli/types.d.ts +22 -0
  69. package/dist/src/cli/types.d.ts.map +1 -0
  70. package/dist/src/cli/types.js +86 -0
  71. package/dist/src/cli/types.js.map +1 -0
  72. package/dist/src/contracts/mcp.js +34 -34
  73. package/dist/src/contracts/schemas.js.map +1 -1
  74. package/dist/src/server/mcp-server.d.ts.map +1 -1
  75. package/dist/src/server/mcp-server.js +7 -14
  76. package/dist/src/server/mcp-server.js.map +1 -1
  77. package/dist/src/services/embedding-service.d.ts.map +1 -1
  78. package/dist/src/services/embedding-service.js +1 -1
  79. package/dist/src/services/embedding-service.js.map +1 -1
  80. package/dist/src/services/memory-service.d.ts.map +1 -1
  81. package/dist/src/services/memory-service.js.map +1 -1
  82. package/dist/src/storage/markdown-storage.d.ts.map +1 -1
  83. package/dist/src/storage/markdown-storage.js +1 -1
  84. package/dist/src/storage/markdown-storage.js.map +1 -1
  85. package/dist/src/storage/vector-index.d.ts.map +1 -1
  86. package/dist/src/storage/vector-index.js +4 -5
  87. package/dist/src/storage/vector-index.js.map +1 -1
  88. package/docs/README.md +21 -0
  89. package/docs/mcp-tools.md +136 -0
  90. package/docs/user-guide.md +182 -0
  91. package/package.json +61 -59
  92. package/src/cli/agents/claude-code.ts +14 -0
  93. package/src/cli/agents/cline.ts +14 -0
  94. package/src/cli/agents/codex.ts +14 -0
  95. package/src/cli/agents/cursor.ts +14 -0
  96. package/src/cli/agents/factory-droid.ts +14 -0
  97. package/src/cli/agents/gemini-cli.ts +14 -0
  98. package/src/cli/agents/index.ts +36 -0
  99. package/src/cli/agents/windsurf.ts +14 -0
  100. package/src/cli/index.ts +192 -0
  101. package/src/cli/init.ts +218 -0
  102. package/src/cli/instructions.ts +156 -0
  103. package/src/cli/types.ts +112 -0
  104. package/src/contracts/index.ts +12 -12
  105. package/src/contracts/mcp.ts +223 -223
  106. package/src/contracts/schemas.ts +307 -307
  107. package/src/contracts/types.ts +410 -410
  108. package/src/index.ts +8 -8
  109. package/src/server/index.ts +5 -5
  110. package/src/server/mcp-server.ts +169 -186
  111. package/src/services/embedding-service.ts +114 -114
  112. package/src/services/index.ts +5 -5
  113. package/src/services/memory-service.ts +656 -663
  114. package/src/storage/frontmatter-parser.ts +243 -243
  115. package/src/storage/index.ts +6 -6
  116. package/src/storage/markdown-storage.ts +228 -236
  117. package/src/storage/vector-index.ts +159 -160
  118. package/src/utils/index.ts +5 -5
  119. package/src/utils/slugify.ts +63 -63
  120. package/test/cli/init.test.ts +380 -0
  121. package/test/contracts/schemas.test.ts +313 -313
  122. package/test/contracts/types.test.ts +21 -21
  123. package/test/frontmatter-parser-more.test.ts +94 -94
  124. package/test/server/mcp-server.test.ts +211 -210
  125. package/test/services/memory-service-edge.test.ts +248 -248
  126. package/test/services/memory-service.test.ts +291 -279
  127. package/test/storage/frontmatter-parser.test.ts +223 -223
  128. package/test/storage/markdown-storage.test.ts +226 -217
  129. package/test/storage/storage-edge.test.ts +238 -238
  130. package/test/storage/vector-index.test.ts +149 -153
  131. package/test/utils/slugify-edge.test.ts +94 -94
  132. package/test/utils/slugify.test.ts +72 -68
  133. package/tsconfig.json +25 -25
  134. package/tsconfig.test.json +8 -8
  135. package/vitest.config.ts +29 -29
  136. package/docs/architecture-diagrams.md +0 -368
  137. package/docs/architecture.md +0 -381
  138. package/docs/contracts.md +0 -190
  139. package/docs/prompt-template.md +0 -33
  140. package/docs/proposals/mcp-typescript-sdk-refactor.md +0 -568
  141. package/docs/proposals/proposal-close-gates.md +0 -58
  142. package/docs/tool-calling-policy.md +0 -101
  143. package/docs/vector-search.md +0 -306
@@ -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,210 +1,211 @@
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
- });
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 =
207
+ process.platform === 'win32' ? 'C:\\myproject\\.memhub' : '/myproject/.memhub';
208
+ expect(resolveStoragePath()).toBe(expected);
209
+ });
210
+ });
211
+ });