memory-journal-mcp 4.3.0 → 4.4.0

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 (109) hide show
  1. package/.dockerignore +131 -122
  2. package/.gitattributes +29 -0
  3. package/.github/workflows/docker-publish.yml +1 -1
  4. package/.github/workflows/lint-and-test.yml +1 -2
  5. package/.github/workflows/secrets-scanning.yml +0 -1
  6. package/.github/workflows/security-update.yml +6 -6
  7. package/.vscode/settings.json +17 -15
  8. package/CHANGELOG.md +1065 -11
  9. package/DOCKER_README.md +51 -33
  10. package/Dockerfile +14 -12
  11. package/README.md +68 -33
  12. package/SECURITY.md +225 -220
  13. package/dist/cli.js +7 -0
  14. package/dist/cli.js.map +1 -1
  15. package/dist/constants/ServerInstructions.d.ts +1 -1
  16. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  17. package/dist/constants/ServerInstructions.js +70 -26
  18. package/dist/constants/ServerInstructions.js.map +1 -1
  19. package/dist/constants/icons.d.ts +2 -0
  20. package/dist/constants/icons.d.ts.map +1 -1
  21. package/dist/constants/icons.js +6 -0
  22. package/dist/constants/icons.js.map +1 -1
  23. package/dist/database/SqliteAdapter.d.ts +51 -10
  24. package/dist/database/SqliteAdapter.d.ts.map +1 -1
  25. package/dist/database/SqliteAdapter.js +143 -43
  26. package/dist/database/SqliteAdapter.js.map +1 -1
  27. package/dist/filtering/ToolFilter.d.ts +1 -1
  28. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  29. package/dist/filtering/ToolFilter.js +7 -1
  30. package/dist/filtering/ToolFilter.js.map +1 -1
  31. package/dist/github/GitHubIntegration.d.ts +74 -2
  32. package/dist/github/GitHubIntegration.d.ts.map +1 -1
  33. package/dist/github/GitHubIntegration.js +508 -7
  34. package/dist/github/GitHubIntegration.js.map +1 -1
  35. package/dist/handlers/prompts/index.js +1 -0
  36. package/dist/handlers/prompts/index.js.map +1 -1
  37. package/dist/handlers/resources/index.d.ts.map +1 -1
  38. package/dist/handlers/resources/index.js +257 -13
  39. package/dist/handlers/resources/index.js.map +1 -1
  40. package/dist/handlers/tools/index.d.ts.map +1 -1
  41. package/dist/handlers/tools/index.js +595 -8
  42. package/dist/handlers/tools/index.js.map +1 -1
  43. package/dist/server/McpServer.d.ts +2 -0
  44. package/dist/server/McpServer.d.ts.map +1 -1
  45. package/dist/server/McpServer.js +69 -26
  46. package/dist/server/McpServer.js.map +1 -1
  47. package/dist/types/index.d.ts +97 -0
  48. package/dist/types/index.d.ts.map +1 -1
  49. package/dist/types/index.js.map +1 -1
  50. package/dist/utils/logger.d.ts +1 -0
  51. package/dist/utils/logger.d.ts.map +1 -1
  52. package/dist/utils/logger.js +8 -1
  53. package/dist/utils/logger.js.map +1 -1
  54. package/dist/utils/progress-utils.d.ts +18 -3
  55. package/dist/utils/progress-utils.d.ts.map +1 -1
  56. package/dist/utils/progress-utils.js.map +1 -1
  57. package/dist/utils/security-utils.d.ts +91 -0
  58. package/dist/utils/security-utils.d.ts.map +1 -0
  59. package/dist/utils/security-utils.js +184 -0
  60. package/dist/utils/security-utils.js.map +1 -0
  61. package/dist/vector/VectorSearchManager.d.ts +2 -1
  62. package/dist/vector/VectorSearchManager.d.ts.map +1 -1
  63. package/dist/vector/VectorSearchManager.js +100 -34
  64. package/dist/vector/VectorSearchManager.js.map +1 -1
  65. package/docker-compose.yml +46 -37
  66. package/mcp-config-example.json +0 -2
  67. package/package.json +21 -14
  68. package/releases/v4.3.1.md +69 -0
  69. package/releases/v4.4.0.md +120 -0
  70. package/server.json +3 -3
  71. package/src/cli.ts +11 -0
  72. package/src/constants/ServerInstructions.ts +70 -26
  73. package/src/constants/icons.ts +7 -0
  74. package/src/database/SqliteAdapter.ts +165 -44
  75. package/src/filtering/ToolFilter.ts +7 -1
  76. package/src/github/GitHubIntegration.ts +588 -8
  77. package/src/handlers/prompts/index.ts +1 -0
  78. package/src/handlers/resources/index.ts +318 -12
  79. package/src/handlers/tools/index.ts +686 -13
  80. package/src/server/McpServer.ts +79 -37
  81. package/src/types/index.ts +98 -0
  82. package/src/utils/logger.ts +10 -1
  83. package/src/utils/progress-utils.ts +17 -6
  84. package/src/utils/security-utils.ts +205 -0
  85. package/src/vector/VectorSearchManager.ts +110 -39
  86. package/tests/constants/icons.test.ts +102 -0
  87. package/tests/constants/server-instructions.test.ts +549 -0
  88. package/tests/database/sqlite-adapter.bench.ts +63 -0
  89. package/tests/database/sqlite-adapter.test.ts +555 -0
  90. package/tests/filtering/tool-filter.test.ts +266 -0
  91. package/tests/github/github-integration.test.ts +1024 -0
  92. package/tests/handlers/github-resource-handlers.test.ts +473 -0
  93. package/tests/handlers/github-tool-handlers.test.ts +556 -0
  94. package/tests/handlers/prompt-handlers.test.ts +91 -0
  95. package/tests/handlers/resource-handlers.test.ts +339 -0
  96. package/tests/handlers/tool-handlers.test.ts +497 -0
  97. package/tests/handlers/vector-tool-handlers.test.ts +238 -0
  98. package/tests/security/sql-injection.test.ts +347 -0
  99. package/tests/server/mcp-server.bench.ts +55 -0
  100. package/tests/server/mcp-server.test.ts +675 -0
  101. package/tests/utils/logger.test.ts +180 -0
  102. package/tests/utils/mcp-logger.test.ts +212 -0
  103. package/tests/utils/progress-utils.test.ts +156 -0
  104. package/tests/utils/security-utils.test.ts +82 -0
  105. package/tests/vector/vector-search-manager.test.ts +335 -0
  106. package/tests/vector/vector-search.bench.ts +53 -0
  107. package/vitest.config.ts +15 -0
  108. package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
  109. package/.github/workflows/dependabot-auto-merge.yml +0 -42
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Tool Handler Tests
3
+ *
4
+ * Tests getTools listing, callTool for non-GitHub tools,
5
+ * and Zod output schema validation.
6
+ */
7
+
8
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest'
9
+ import { getTools, callTool } from '../../src/handlers/tools/index.js'
10
+ import { SqliteAdapter } from '../../src/database/SqliteAdapter.js'
11
+
12
+ describe('Tool Handlers', () => {
13
+ let db: SqliteAdapter
14
+ const testDbPath = './test-tools.db'
15
+
16
+ beforeAll(async () => {
17
+ db = new SqliteAdapter(testDbPath)
18
+ await db.initialize()
19
+ })
20
+
21
+ afterAll(() => {
22
+ db.close()
23
+ try {
24
+ const fs = require('node:fs')
25
+ if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath)
26
+ } catch {
27
+ // Ignore cleanup errors
28
+ }
29
+ })
30
+
31
+ // ========================================================================
32
+ // getTools
33
+ // ========================================================================
34
+
35
+ describe('getTools', () => {
36
+ it('should return all tools when no filter', () => {
37
+ const tools = getTools(db, null)
38
+ expect(tools.length).toBeGreaterThan(30)
39
+ })
40
+
41
+ it('should have name, description, and inputSchema on each tool', () => {
42
+ const tools = getTools(db, null)
43
+ for (const t of tools) {
44
+ const tool = t as {
45
+ name: string
46
+ description: string
47
+ inputSchema: object
48
+ }
49
+ expect(typeof tool.name).toBe('string')
50
+ expect(typeof tool.description).toBe('string')
51
+ expect(tool.inputSchema).toBeDefined()
52
+ }
53
+ })
54
+
55
+ it('should have outputSchema on each tool (MCP 2025-11-25)', () => {
56
+ const tools = getTools(db, null)
57
+ for (const t of tools) {
58
+ const tool = t as { name: string; outputSchema?: object }
59
+ expect(tool.outputSchema).toBeDefined()
60
+ }
61
+ })
62
+
63
+ it('should include known tool names', () => {
64
+ const tools = getTools(db, null)
65
+ const names = tools.map((t) => (t as { name: string }).name)
66
+
67
+ expect(names).toContain('create_entry')
68
+ expect(names).toContain('search_entries')
69
+ expect(names).toContain('get_recent_entries')
70
+ expect(names).toContain('get_statistics')
71
+ expect(names).toContain('link_entries')
72
+ expect(names).toContain('backup_journal')
73
+ })
74
+ })
75
+
76
+ // ========================================================================
77
+ // callTool - core tools
78
+ // ========================================================================
79
+
80
+ describe('callTool - create_entry', () => {
81
+ it('should create a basic entry', async () => {
82
+ const result = (await callTool(
83
+ 'create_entry',
84
+ { content: 'Tool handler test entry' },
85
+ db
86
+ )) as { success: boolean; entry: { id: number; content: string } }
87
+
88
+ expect(result.success).toBe(true)
89
+ expect(result.entry).toBeDefined()
90
+ expect(result.entry.id).toBeGreaterThan(0)
91
+ expect(result.entry.content).toBe('Tool handler test entry')
92
+ })
93
+
94
+ it('should create entry with all fields', async () => {
95
+ const result = (await callTool(
96
+ 'create_entry',
97
+ {
98
+ content: 'Full tool entry',
99
+ entry_type: 'decision',
100
+ tags: ['tool-tag-a', 'tool-tag-b'],
101
+ is_personal: false,
102
+ significance_type: 'milestone',
103
+ },
104
+ db
105
+ )) as { success: boolean; entry: { entryType: string; tags: string[] } }
106
+
107
+ expect(result.success).toBe(true)
108
+ expect(result.entry.entryType).toBe('decision')
109
+ expect(result.entry.tags).toContain('tool-tag-a')
110
+ })
111
+ })
112
+
113
+ describe('callTool - create_entry_minimal', () => {
114
+ it('should create a minimal entry', async () => {
115
+ const result = (await callTool(
116
+ 'create_entry_minimal',
117
+ { content: 'Quick note' },
118
+ db
119
+ )) as { success: boolean; entry: { content: string } }
120
+
121
+ expect(result.success).toBe(true)
122
+ expect(result.entry.content).toBe('Quick note')
123
+ })
124
+ })
125
+
126
+ describe('callTool - get_entry_by_id', () => {
127
+ it('should retrieve an entry by ID', async () => {
128
+ const created = (await callTool(
129
+ 'create_entry',
130
+ { content: 'Retrievable entry' },
131
+ db
132
+ )) as { entry: { id: number } }
133
+
134
+ const result = (await callTool(
135
+ 'get_entry_by_id',
136
+ { entry_id: created.entry.id },
137
+ db
138
+ )) as { entry: { content: string }; importance: number }
139
+
140
+ expect(result.entry.content).toBe('Retrievable entry')
141
+ expect(typeof result.importance).toBe('number')
142
+ })
143
+
144
+ it('should return error for nonexistent entry', async () => {
145
+ const result = (await callTool('get_entry_by_id', { entry_id: 99999 }, db)) as {
146
+ error: string
147
+ }
148
+
149
+ expect(result.error).toContain('not found')
150
+ })
151
+ })
152
+
153
+ describe('callTool - get_recent_entries', () => {
154
+ it('should return recent entries', async () => {
155
+ const result = (await callTool('get_recent_entries', { limit: 3 }, db)) as {
156
+ entries: unknown[]
157
+ count: number
158
+ }
159
+
160
+ expect(result.entries.length).toBeLessThanOrEqual(3)
161
+ expect(result.count).toBeGreaterThan(0)
162
+ })
163
+ })
164
+
165
+ describe('callTool - test_simple', () => {
166
+ it('should echo message back', async () => {
167
+ const result = (await callTool('test_simple', { message: 'ping' }, db)) as {
168
+ message: string
169
+ }
170
+
171
+ expect(result.message).toContain('ping')
172
+ })
173
+ })
174
+
175
+ // ========================================================================
176
+ // callTool - search tools
177
+ // ========================================================================
178
+
179
+ describe('callTool - search_entries', () => {
180
+ it('should search by content', async () => {
181
+ await callTool('create_entry', { content: 'Unique unicorn xyz99' }, db)
182
+
183
+ const result = (await callTool('search_entries', { query: 'xyz99', limit: 5 }, db)) as {
184
+ entries: unknown[]
185
+ count: number
186
+ }
187
+
188
+ expect(result.count).toBeGreaterThan(0)
189
+ })
190
+
191
+ it('should return empty for non-matching query', async () => {
192
+ const result = (await callTool(
193
+ 'search_entries',
194
+ { query: 'nonexistent_term_abcxyz', limit: 5 },
195
+ db
196
+ )) as { entries: unknown[]; count: number }
197
+
198
+ expect(result.count).toBe(0)
199
+ })
200
+ })
201
+
202
+ describe('callTool - search_by_date_range', () => {
203
+ it('should search by date range', async () => {
204
+ const today = new Date().toISOString().split('T')[0]!
205
+ const result = (await callTool(
206
+ 'search_by_date_range',
207
+ { start_date: today, end_date: today },
208
+ db
209
+ )) as { entries: unknown[]; count: number }
210
+
211
+ expect(result.count).toBeGreaterThan(0)
212
+ })
213
+ })
214
+
215
+ // ========================================================================
216
+ // callTool - analytics tools
217
+ // ========================================================================
218
+
219
+ describe('callTool - get_statistics', () => {
220
+ it('should return statistics', async () => {
221
+ const result = (await callTool('get_statistics', {}, db)) as {
222
+ groupBy: string
223
+ totalEntries: number
224
+ }
225
+
226
+ expect(result.totalEntries).toBeGreaterThan(0)
227
+ expect(result.groupBy).toBe('week')
228
+ })
229
+
230
+ it('should accept group_by parameter', async () => {
231
+ const result = (await callTool('get_statistics', { group_by: 'day' }, db)) as {
232
+ groupBy: string
233
+ }
234
+
235
+ expect(result.groupBy).toBe('day')
236
+ })
237
+ })
238
+
239
+ // ========================================================================
240
+ // callTool - relationship tools
241
+ // ========================================================================
242
+
243
+ describe('callTool - link_entries', () => {
244
+ it('should link two entries', async () => {
245
+ const e1 = (await callTool('create_entry', { content: 'Link source' }, db)) as {
246
+ entry: { id: number }
247
+ }
248
+ const e2 = (await callTool('create_entry', { content: 'Link target' }, db)) as {
249
+ entry: { id: number }
250
+ }
251
+
252
+ const result = (await callTool(
253
+ 'link_entries',
254
+ {
255
+ from_entry_id: e1.entry.id,
256
+ to_entry_id: e2.entry.id,
257
+ relationship_type: 'references',
258
+ description: 'Test link',
259
+ },
260
+ db
261
+ )) as { success: boolean; relationship: { relationshipType: string } }
262
+
263
+ expect(result.success).toBe(true)
264
+ expect(result.relationship.relationshipType).toBe('references')
265
+ })
266
+
267
+ it('should return failure for nonexistent entry', async () => {
268
+ const result = (await callTool(
269
+ 'link_entries',
270
+ {
271
+ from_entry_id: 99999,
272
+ to_entry_id: 99998,
273
+ relationship_type: 'references',
274
+ },
275
+ db
276
+ )) as { success: boolean; message?: string }
277
+
278
+ expect(result.success).toBe(false)
279
+ })
280
+ })
281
+
282
+ // ========================================================================
283
+ // callTool - admin tools
284
+ // ========================================================================
285
+
286
+ describe('callTool - update_entry', () => {
287
+ it('should update entry content', async () => {
288
+ const created = (await callTool(
289
+ 'create_entry',
290
+ { content: 'Original content' },
291
+ db
292
+ )) as { entry: { id: number } }
293
+
294
+ const result = (await callTool(
295
+ 'update_entry',
296
+ { entry_id: created.entry.id, content: 'Updated content' },
297
+ db
298
+ )) as { success: boolean; entry: { content: string } }
299
+
300
+ expect(result.success).toBe(true)
301
+ expect(result.entry.content).toBe('Updated content')
302
+ })
303
+ })
304
+
305
+ describe('callTool - delete_entry', () => {
306
+ it('should soft delete an entry', async () => {
307
+ const created = (await callTool('create_entry', { content: 'To be deleted' }, db)) as {
308
+ entry: { id: number }
309
+ }
310
+
311
+ const result = (await callTool('delete_entry', { entry_id: created.entry.id }, db)) as {
312
+ success: boolean
313
+ entryId: number
314
+ }
315
+
316
+ expect(result.success).toBe(true)
317
+ expect(result.entryId).toBe(created.entry.id)
318
+ })
319
+
320
+ it('should return error for nonexistent entry', async () => {
321
+ const result = (await callTool('delete_entry', { entry_id: 99999 }, db)) as {
322
+ success: boolean
323
+ error: string
324
+ }
325
+
326
+ expect(result.success).toBe(false)
327
+ expect(result.error).toContain('not found')
328
+ })
329
+ })
330
+
331
+ // ========================================================================
332
+ // callTool - tag tools
333
+ // ========================================================================
334
+
335
+ describe('callTool - list_tags', () => {
336
+ it('should list tags with usage counts', async () => {
337
+ const result = (await callTool('list_tags', {}, db)) as {
338
+ tags: { name: string; count: number }[]
339
+ count: number
340
+ }
341
+
342
+ expect(result.tags).toBeDefined()
343
+ expect(result.count).toBeGreaterThan(0)
344
+ })
345
+ })
346
+
347
+ describe('callTool - merge_tags', () => {
348
+ it('should merge tags', async () => {
349
+ await callTool('create_entry', { content: 'Merge tag source', tags: ['merge-src'] }, db)
350
+
351
+ const result = (await callTool(
352
+ 'merge_tags',
353
+ { source_tag: 'merge-src', target_tag: 'merge-tgt' },
354
+ db
355
+ )) as { success: boolean; message: string }
356
+
357
+ expect(result.success).toBe(true)
358
+ expect(result.message).toContain('Merged')
359
+ })
360
+ })
361
+
362
+ // ========================================================================
363
+ // callTool - export tools
364
+ // ========================================================================
365
+
366
+ describe('callTool - export_entries', () => {
367
+ it('should export as JSON', async () => {
368
+ const result = (await callTool('export_entries', { format: 'json' }, db)) as {
369
+ format: string
370
+ entries: unknown[]
371
+ }
372
+
373
+ expect(result.format).toBe('json')
374
+ expect(result.entries.length).toBeGreaterThan(0)
375
+ })
376
+
377
+ it('should export as markdown', async () => {
378
+ const result = (await callTool('export_entries', { format: 'markdown' }, db)) as {
379
+ format: string
380
+ content: string
381
+ }
382
+
383
+ expect(result.format).toBe('markdown')
384
+ expect(result.content.length).toBeGreaterThan(0)
385
+ })
386
+ })
387
+
388
+ // ========================================================================
389
+ // callTool - backup tools
390
+ // ========================================================================
391
+
392
+ describe('callTool - backup_journal', () => {
393
+ it('should create a backup', async () => {
394
+ const result = (await callTool('backup_journal', { name: 'test-tool-backup' }, db)) as {
395
+ success: boolean
396
+ filename: string
397
+ path: string
398
+ sizeBytes: number
399
+ }
400
+
401
+ expect(result.success).toBe(true)
402
+ expect(result.filename).toContain('test-tool-backup')
403
+
404
+ // Cleanup backup file
405
+ try {
406
+ const fs = require('node:fs')
407
+ if (fs.existsSync(result.path)) {
408
+ fs.unlinkSync(result.path)
409
+ }
410
+ } catch {
411
+ // Ignore cleanup
412
+ }
413
+ })
414
+ })
415
+
416
+ describe('callTool - list_backups', () => {
417
+ it('should list backups', async () => {
418
+ const result = (await callTool('list_backups', {}, db)) as {
419
+ backups: unknown[]
420
+ total: number
421
+ backupsDirectory: string
422
+ }
423
+
424
+ expect(result.backups).toBeDefined()
425
+ expect(typeof result.total).toBe('number')
426
+ expect(result.backupsDirectory).toBeDefined()
427
+ })
428
+ })
429
+
430
+ // ========================================================================
431
+ // callTool - unknown tool
432
+ // ========================================================================
433
+
434
+ describe('callTool - error handling', () => {
435
+ it('should throw for unknown tool', async () => {
436
+ await expect(callTool('nonexistent_tool', {}, db)).rejects.toThrow('Unknown tool')
437
+ })
438
+ })
439
+
440
+ // ========================================================================
441
+ // callTool - visualize_relationships
442
+ // ========================================================================
443
+
444
+ describe('callTool - visualize_relationships', () => {
445
+ it('should generate mermaid diagram', async () => {
446
+ const e1 = (await callTool('create_entry', { content: 'Viz entry A' }, db)) as {
447
+ entry: { id: number }
448
+ }
449
+ const e2 = (await callTool('create_entry', { content: 'Viz entry B' }, db)) as {
450
+ entry: { id: number }
451
+ }
452
+ await callTool(
453
+ 'link_entries',
454
+ {
455
+ from_entry_id: e1.entry.id,
456
+ to_entry_id: e2.entry.id,
457
+ relationship_type: 'implements',
458
+ },
459
+ db
460
+ )
461
+
462
+ const result = (await callTool(
463
+ 'visualize_relationships',
464
+ { entry_id: e1.entry.id },
465
+ db
466
+ )) as { mermaid: string | null; entry_count: number }
467
+
468
+ expect(result.entry_count).toBeGreaterThan(0)
469
+ })
470
+ })
471
+
472
+ // ========================================================================
473
+ // callTool - cross project insights
474
+ // ========================================================================
475
+
476
+ describe('callTool - get_cross_project_insights', () => {
477
+ it('should return insights structure', async () => {
478
+ // Need project entries for insights
479
+ await callTool('create_entry', { content: 'Insight entry 1', project_number: 100 }, db)
480
+ await callTool('create_entry', { content: 'Insight entry 2', project_number: 100 }, db)
481
+ await callTool('create_entry', { content: 'Insight entry 3', project_number: 100 }, db)
482
+
483
+ const result = (await callTool(
484
+ 'get_cross_project_insights',
485
+ { min_entries: 1 },
486
+ db
487
+ )) as {
488
+ project_count: number
489
+ total_entries: number
490
+ projects: unknown[]
491
+ }
492
+
493
+ expect(result.project_count).toBeGreaterThan(0)
494
+ expect(result.total_entries).toBeGreaterThan(0)
495
+ })
496
+ })
497
+ })