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,339 @@
1
+ /**
2
+ * Resource Handler Tests
3
+ *
4
+ * Tests getResources listing and readResource for non-GitHub resources.
5
+ */
6
+
7
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest'
8
+ import { getResources, readResource } from '../../src/handlers/resources/index.js'
9
+ import { SqliteAdapter } from '../../src/database/SqliteAdapter.js'
10
+
11
+ describe('Resource Handlers', () => {
12
+ let db: SqliteAdapter
13
+ const testDbPath = './test-resources.db'
14
+
15
+ beforeAll(async () => {
16
+ db = new SqliteAdapter(testDbPath)
17
+ await db.initialize()
18
+
19
+ // Seed test data
20
+ const e1 = db.createEntry({
21
+ content: 'Resource test entry alpha',
22
+ tags: ['res-tag'],
23
+ significanceType: 'milestone',
24
+ })
25
+ const e2 = db.createEntry({
26
+ content: 'Resource test entry beta',
27
+ isPersonal: false,
28
+ projectNumber: 42,
29
+ issueNumber: 7,
30
+ })
31
+ const e3 = db.createEntry({
32
+ content: 'Resource test entry gamma',
33
+ prNumber: 15,
34
+ })
35
+ db.linkEntries(e1.id, e2.id, 'references', 'Related work')
36
+ })
37
+
38
+ afterAll(() => {
39
+ db.close()
40
+ try {
41
+ const fs = require('node:fs')
42
+ if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath)
43
+ } catch {
44
+ // Ignore cleanup errors
45
+ }
46
+ })
47
+
48
+ // ========================================================================
49
+ // getResources
50
+ // ========================================================================
51
+
52
+ describe('getResources', () => {
53
+ it('should return non-empty array of resource definitions', () => {
54
+ const resources = getResources()
55
+ expect(Array.isArray(resources)).toBe(true)
56
+ expect(resources.length).toBeGreaterThan(10)
57
+ })
58
+
59
+ it('should have uri, name, and description on each resource', () => {
60
+ const resources = getResources()
61
+ for (const r of resources) {
62
+ const res = r as { uri: string; name: string; description: string }
63
+ expect(typeof res.uri).toBe('string')
64
+ expect(res.uri).toMatch(/^memory:\/\//)
65
+ expect(typeof res.name).toBe('string')
66
+ expect(typeof res.description).toBe('string')
67
+ }
68
+ })
69
+
70
+ it('should include known resource URIs', () => {
71
+ const resources = getResources()
72
+ const uris = resources.map((r) => (r as { uri: string }).uri)
73
+
74
+ expect(uris).toContain('memory://briefing')
75
+ expect(uris).toContain('memory://health')
76
+ expect(uris).toContain('memory://recent')
77
+ expect(uris).toContain('memory://significant')
78
+ expect(uris).toContain('memory://tags')
79
+ expect(uris).toContain('memory://team/recent')
80
+ })
81
+
82
+ it('should include template resources', () => {
83
+ const resources = getResources()
84
+ const uris = resources.map((r) => (r as { uri: string }).uri)
85
+
86
+ expect(uris).toContain('memory://projects/{number}/timeline')
87
+ expect(uris).toContain('memory://issues/{issue_number}/entries')
88
+ expect(uris).toContain('memory://prs/{pr_number}/entries')
89
+ })
90
+ })
91
+
92
+ // ========================================================================
93
+ // readResource - static resources
94
+ // ========================================================================
95
+
96
+ describe('readResource - static resources', () => {
97
+ it('should read memory://briefing', async () => {
98
+ const result = await readResource('memory://briefing', db)
99
+
100
+ expect(result.data).toBeDefined()
101
+ const data = result.data as {
102
+ version: string
103
+ journal: { totalEntries: number }
104
+ userMessage: string
105
+ }
106
+ expect(data.version).toBeDefined()
107
+ expect(data.journal.totalEntries).toBeGreaterThan(0)
108
+ expect(data.userMessage).toContain('Session Context')
109
+ })
110
+
111
+ it('should read memory://instructions', async () => {
112
+ const result = await readResource('memory://instructions', db)
113
+
114
+ expect(result.data).toBeDefined()
115
+ expect(typeof result.data).toBe('string')
116
+ expect(result.data as string).toContain('Session Start')
117
+ })
118
+
119
+ it('should show all tools in memory://instructions when no filter is set', async () => {
120
+ // Bug fix: when filterConfig is null (all tools enabled), memory://instructions
121
+ // must show Active Tools (39), not Active Tools (3) from the old hardcoded fallback
122
+ const result = await readResource('memory://instructions', db, undefined, null)
123
+
124
+ const text = result.data as string
125
+ expect(text).toContain('Active Tools (39)')
126
+ })
127
+
128
+ it('should read memory://recent', async () => {
129
+ const result = await readResource('memory://recent', db)
130
+
131
+ const data = result.data as { entries: unknown[]; count: number }
132
+ expect(data.entries).toBeDefined()
133
+ expect(data.count).toBeGreaterThan(0)
134
+ expect(result.annotations?.lastModified).toBeDefined()
135
+ })
136
+
137
+ it('should read memory://significant', async () => {
138
+ const result = await readResource('memory://significant', db)
139
+
140
+ const data = result.data as { entries: unknown[]; count: number }
141
+ expect(data.entries).toBeDefined()
142
+ // We created an entry with significanceType='milestone'
143
+ expect(data.count).toBeGreaterThan(0)
144
+ })
145
+
146
+ it('should sort memory://significant by importance descending, not by timestamp', async () => {
147
+ // Create 3 significant entries: e2 gets a relationship (higher importance), e3 gets 2 (highest)
148
+ const eBase = db.createEntry({ content: 'Sort test: base entry (no relationships)' })
149
+ const e1 = db.createEntry({
150
+ content: 'Sort test: sig entry 1 - no relationships',
151
+ significanceType: 'milestone',
152
+ })
153
+ const e2 = db.createEntry({
154
+ content: 'Sort test: sig entry 2 - one relationship',
155
+ significanceType: 'blocker_resolved',
156
+ })
157
+ const e3 = db.createEntry({
158
+ content: 'Sort test: sig entry 3 - two relationships',
159
+ significanceType: 'milestone',
160
+ })
161
+ // Give e2 one relationship, e3 two relationships → e3 should score highest
162
+ db.linkEntries(e2.id, eBase.id, 'references', 'One link')
163
+ db.linkEntries(e3.id, eBase.id, 'references', 'Link A')
164
+ db.linkEntries(e3.id, e1.id, 'references', 'Link B')
165
+
166
+ const result = await readResource('memory://significant', db)
167
+ const data = result.data as {
168
+ entries: { id: number; importance: number }[]
169
+ count: number
170
+ }
171
+
172
+ // 1: importance field present on all entries
173
+ for (const entry of data.entries) {
174
+ expect(typeof entry.importance).toBe('number')
175
+ }
176
+
177
+ // 2: entries sorted descending by importance (no entry should be less important than its successor)
178
+ for (let i = 1; i < data.entries.length; i++) {
179
+ const prev = data.entries[i - 1]!
180
+ const curr = data.entries[i]!
181
+ expect(prev.importance).toBeGreaterThanOrEqual(curr.importance)
182
+ }
183
+ })
184
+
185
+ it('should read memory://tags', async () => {
186
+ const result = await readResource('memory://tags', db)
187
+
188
+ const data = result.data as { tags: unknown[]; count: number }
189
+ expect(data.tags).toBeDefined()
190
+ expect(data.count).toBeGreaterThan(0)
191
+ })
192
+
193
+ it('should read memory://health', async () => {
194
+ const result = await readResource('memory://health', db)
195
+
196
+ const data = result.data as {
197
+ database: { entryCount: number }
198
+ toolFilter: { totalCount: number }
199
+ timestamp: string
200
+ }
201
+ expect(data.database.entryCount).toBeGreaterThan(0)
202
+ expect(data.toolFilter.totalCount).toBeGreaterThan(0)
203
+ expect(data.timestamp).toBeDefined()
204
+ })
205
+
206
+ it('should read memory://team/recent', async () => {
207
+ const result = await readResource('memory://team/recent', db)
208
+
209
+ const data = result.data as { entries: unknown[]; count: number }
210
+ expect(data.entries).toBeDefined()
211
+ })
212
+
213
+ it('should read memory://graph/recent', async () => {
214
+ const result = await readResource('memory://graph/recent', db)
215
+
216
+ const data = result.data as {
217
+ format: string
218
+ diagram: string
219
+ relationshipCount?: number
220
+ }
221
+ expect(data.format).toBe('mermaid')
222
+ expect(data.diagram).toContain('graph TD')
223
+ })
224
+
225
+ it('should read memory://statistics', async () => {
226
+ const result = await readResource('memory://statistics', db)
227
+
228
+ const data = result.data as { totalEntries: number; entriesByType: object }
229
+ expect(data.totalEntries).toBeGreaterThan(0)
230
+ expect(data.entriesByType).toBeDefined()
231
+ })
232
+ })
233
+
234
+ // ========================================================================
235
+ // readResource - template resources
236
+ // ========================================================================
237
+
238
+ describe('readResource - template resources', () => {
239
+ it('should read memory://projects/{number}/timeline', async () => {
240
+ const result = await readResource('memory://projects/42/timeline', db)
241
+
242
+ const data = result.data as { projectNumber: number; entries: unknown[]; count: number }
243
+ expect(data.projectNumber).toBe(42)
244
+ expect(data.count).toBeGreaterThan(0)
245
+ })
246
+
247
+ it('should read memory://issues/{issue_number}/entries', async () => {
248
+ const result = await readResource('memory://issues/7/entries', db)
249
+
250
+ const data = result.data as { issueNumber: number; entries: unknown[]; count: number }
251
+ expect(data.issueNumber).toBe(7)
252
+ expect(data.count).toBeGreaterThan(0)
253
+ })
254
+
255
+ it('should read memory://prs/{pr_number}/entries', async () => {
256
+ const result = await readResource('memory://prs/15/entries', db)
257
+
258
+ const data = result.data as { prNumber: number; entries: unknown[]; count: number }
259
+ expect(data.prNumber).toBe(15)
260
+ expect(data.count).toBeGreaterThan(0)
261
+ })
262
+
263
+ it('should return empty for non-matching project', async () => {
264
+ const result = await readResource('memory://projects/99999/timeline', db)
265
+
266
+ const data = result.data as { count: number }
267
+ expect(data.count).toBe(0)
268
+ })
269
+
270
+ it('should handle empty PR entries with hint', async () => {
271
+ const result = await readResource('memory://prs/99999/entries', db)
272
+
273
+ const data = result.data as { count: number; hint?: string }
274
+ expect(data.count).toBe(0)
275
+ expect(data.hint).toContain('No journal entries')
276
+ })
277
+ })
278
+
279
+ // ========================================================================
280
+ // readResource - error cases
281
+ // ========================================================================
282
+
283
+ describe('readResource - error cases', () => {
284
+ it('should throw for unknown resource', async () => {
285
+ await expect(readResource('memory://nonexistent', db)).rejects.toThrow(
286
+ 'Unknown resource'
287
+ )
288
+ })
289
+ })
290
+
291
+ // ========================================================================
292
+ // readResource - branch coverage for query params & variants
293
+ // ========================================================================
294
+
295
+ describe('readResource - additional branch coverage', () => {
296
+ it('should read memory://graph/actions', async () => {
297
+ const result = await readResource('memory://graph/actions', db)
298
+
299
+ const data = result.data as { format: string; diagram: string }
300
+ expect(data.format).toBe('mermaid')
301
+ })
302
+
303
+ it('should read memory://actions/recent', async () => {
304
+ const result = await readResource('memory://actions/recent', db)
305
+
306
+ const data = result.data as { entries: unknown[]; count: number }
307
+ expect(data.entries).toBeDefined()
308
+ })
309
+
310
+ it('should return briefing with expected structure', async () => {
311
+ const result = await readResource('memory://briefing', db)
312
+ const data = result.data as {
313
+ version: string
314
+ journal: { totalEntries: number }
315
+ behaviors: { create: string }
316
+ userMessage: string
317
+ templateResources: string[]
318
+ }
319
+
320
+ expect(data.behaviors.create).toContain('implementations')
321
+ expect(data.templateResources).toContain('memory://projects/{number}/timeline')
322
+ })
323
+
324
+ it('should return annotations on recent entries', async () => {
325
+ const result = await readResource('memory://recent', db)
326
+
327
+ expect(result.annotations).toBeDefined()
328
+ expect(result.annotations?.lastModified).toBeDefined()
329
+ })
330
+
331
+ it('should handle PR timeline template', async () => {
332
+ // PR 15 was seeded with an entry
333
+ const result = await readResource('memory://prs/15/timeline', db)
334
+
335
+ const data = result.data as { prNumber: number }
336
+ expect(data.prNumber).toBe(15)
337
+ })
338
+ })
339
+ })