memory-journal-mcp 4.4.2 → 5.0.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 (291) hide show
  1. package/.github/workflows/codeql.yml +1 -6
  2. package/.github/workflows/docker-publish.yml +15 -49
  3. package/.github/workflows/lint-and-test.yml +1 -1
  4. package/.github/workflows/secrets-scanning.yml +4 -3
  5. package/.github/workflows/security-update.yml +3 -3
  6. package/CHANGELOG.md +213 -0
  7. package/CONTRIBUTING.md +132 -97
  8. package/DOCKER_README.md +184 -235
  9. package/Dockerfile +27 -24
  10. package/README.md +218 -190
  11. package/SECURITY.md +27 -35
  12. package/dist/cli.js +16 -1
  13. package/dist/cli.js.map +1 -1
  14. package/dist/constants/ServerInstructions.d.ts +5 -1
  15. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  16. package/dist/constants/ServerInstructions.js +133 -73
  17. package/dist/constants/ServerInstructions.js.map +1 -1
  18. package/dist/constants/icons.d.ts +2 -2
  19. package/dist/constants/icons.d.ts.map +1 -1
  20. package/dist/constants/icons.js +7 -6
  21. package/dist/constants/icons.js.map +1 -1
  22. package/dist/database/SqliteAdapter.d.ts +37 -24
  23. package/dist/database/SqliteAdapter.d.ts.map +1 -1
  24. package/dist/database/SqliteAdapter.js +319 -157
  25. package/dist/database/SqliteAdapter.js.map +1 -1
  26. package/dist/database/schema.d.ts +45 -0
  27. package/dist/database/schema.d.ts.map +1 -0
  28. package/dist/database/schema.js +92 -0
  29. package/dist/database/schema.js.map +1 -0
  30. package/dist/filtering/ToolFilter.d.ts +1 -1
  31. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  32. package/dist/filtering/ToolFilter.js +13 -2
  33. package/dist/filtering/ToolFilter.js.map +1 -1
  34. package/dist/github/GitHubIntegration.d.ts.map +1 -1
  35. package/dist/github/GitHubIntegration.js +1 -3
  36. package/dist/github/GitHubIntegration.js.map +1 -1
  37. package/dist/handlers/prompts/github.d.ts +12 -0
  38. package/dist/handlers/prompts/github.d.ts.map +1 -0
  39. package/dist/handlers/prompts/github.js +178 -0
  40. package/dist/handlers/prompts/github.js.map +1 -0
  41. package/dist/handlers/prompts/index.d.ts +23 -2
  42. package/dist/handlers/prompts/index.d.ts.map +1 -1
  43. package/dist/handlers/prompts/index.js +7 -432
  44. package/dist/handlers/prompts/index.js.map +1 -1
  45. package/dist/handlers/prompts/workflow.d.ts +12 -0
  46. package/dist/handlers/prompts/workflow.d.ts.map +1 -0
  47. package/dist/handlers/prompts/workflow.js +277 -0
  48. package/dist/handlers/prompts/workflow.js.map +1 -0
  49. package/dist/handlers/resources/core.d.ts +11 -0
  50. package/dist/handlers/resources/core.d.ts.map +1 -0
  51. package/dist/handlers/resources/core.js +433 -0
  52. package/dist/handlers/resources/core.js.map +1 -0
  53. package/dist/handlers/resources/github.d.ts +11 -0
  54. package/dist/handlers/resources/github.d.ts.map +1 -0
  55. package/dist/handlers/resources/github.js +314 -0
  56. package/dist/handlers/resources/github.js.map +1 -0
  57. package/dist/handlers/resources/graph.d.ts +11 -0
  58. package/dist/handlers/resources/graph.d.ts.map +1 -0
  59. package/dist/handlers/resources/graph.js +204 -0
  60. package/dist/handlers/resources/graph.js.map +1 -0
  61. package/dist/handlers/resources/index.d.ts +5 -20
  62. package/dist/handlers/resources/index.d.ts.map +1 -1
  63. package/dist/handlers/resources/index.js +16 -1278
  64. package/dist/handlers/resources/index.js.map +1 -1
  65. package/dist/handlers/resources/shared.d.ts +60 -0
  66. package/dist/handlers/resources/shared.d.ts.map +1 -0
  67. package/dist/handlers/resources/shared.js +49 -0
  68. package/dist/handlers/resources/shared.js.map +1 -0
  69. package/dist/handlers/resources/team.d.ts +13 -0
  70. package/dist/handlers/resources/team.d.ts.map +1 -0
  71. package/dist/handlers/resources/team.js +119 -0
  72. package/dist/handlers/resources/team.js.map +1 -0
  73. package/dist/handlers/resources/templates.d.ts +13 -0
  74. package/dist/handlers/resources/templates.d.ts.map +1 -0
  75. package/dist/handlers/resources/templates.js +310 -0
  76. package/dist/handlers/resources/templates.js.map +1 -0
  77. package/dist/handlers/tools/admin.d.ts +8 -0
  78. package/dist/handlers/tools/admin.d.ts.map +1 -0
  79. package/dist/handlers/tools/admin.js +270 -0
  80. package/dist/handlers/tools/admin.js.map +1 -0
  81. package/dist/handlers/tools/analytics.d.ts +8 -0
  82. package/dist/handlers/tools/analytics.d.ts.map +1 -0
  83. package/dist/handlers/tools/analytics.js +256 -0
  84. package/dist/handlers/tools/analytics.js.map +1 -0
  85. package/dist/handlers/tools/backup.d.ts +8 -0
  86. package/dist/handlers/tools/backup.d.ts.map +1 -0
  87. package/dist/handlers/tools/backup.js +224 -0
  88. package/dist/handlers/tools/backup.js.map +1 -0
  89. package/dist/handlers/tools/core.d.ts +9 -0
  90. package/dist/handlers/tools/core.d.ts.map +1 -0
  91. package/dist/handlers/tools/core.js +326 -0
  92. package/dist/handlers/tools/core.js.map +1 -0
  93. package/dist/handlers/tools/export.d.ts +8 -0
  94. package/dist/handlers/tools/export.d.ts.map +1 -0
  95. package/dist/handlers/tools/export.js +89 -0
  96. package/dist/handlers/tools/export.js.map +1 -0
  97. package/dist/handlers/tools/github/helpers.d.ts +34 -0
  98. package/dist/handlers/tools/github/helpers.d.ts.map +1 -0
  99. package/dist/handlers/tools/github/helpers.js +52 -0
  100. package/dist/handlers/tools/github/helpers.js.map +1 -0
  101. package/dist/handlers/tools/github/insights-tools.d.ts +8 -0
  102. package/dist/handlers/tools/github/insights-tools.d.ts.map +1 -0
  103. package/dist/handlers/tools/github/insights-tools.js +104 -0
  104. package/dist/handlers/tools/github/insights-tools.js.map +1 -0
  105. package/dist/handlers/tools/github/issue-tools.d.ts +8 -0
  106. package/dist/handlers/tools/github/issue-tools.d.ts.map +1 -0
  107. package/dist/handlers/tools/github/issue-tools.js +359 -0
  108. package/dist/handlers/tools/github/issue-tools.js.map +1 -0
  109. package/dist/handlers/tools/github/kanban-tools.d.ts +8 -0
  110. package/dist/handlers/tools/github/kanban-tools.d.ts.map +1 -0
  111. package/dist/handlers/tools/github/kanban-tools.js +108 -0
  112. package/dist/handlers/tools/github/kanban-tools.js.map +1 -0
  113. package/dist/handlers/tools/github/milestone-tools.d.ts +9 -0
  114. package/dist/handlers/tools/github/milestone-tools.d.ts.map +1 -0
  115. package/dist/handlers/tools/github/milestone-tools.js +302 -0
  116. package/dist/handlers/tools/github/milestone-tools.js.map +1 -0
  117. package/dist/handlers/tools/github/mutation-tools.d.ts +12 -0
  118. package/dist/handlers/tools/github/mutation-tools.d.ts.map +1 -0
  119. package/dist/handlers/tools/github/mutation-tools.js +15 -0
  120. package/dist/handlers/tools/github/mutation-tools.js.map +1 -0
  121. package/dist/handlers/tools/github/read-tools.d.ts +8 -0
  122. package/dist/handlers/tools/github/read-tools.d.ts.map +1 -0
  123. package/dist/handlers/tools/github/read-tools.js +260 -0
  124. package/dist/handlers/tools/github/read-tools.js.map +1 -0
  125. package/dist/handlers/tools/github/schemas.d.ts +467 -0
  126. package/dist/handlers/tools/github/schemas.d.ts.map +1 -0
  127. package/dist/handlers/tools/github/schemas.js +335 -0
  128. package/dist/handlers/tools/github/schemas.js.map +1 -0
  129. package/dist/handlers/tools/github.d.ts +14 -0
  130. package/dist/handlers/tools/github.d.ts.map +1 -0
  131. package/dist/handlers/tools/github.js +28 -0
  132. package/dist/handlers/tools/github.js.map +1 -0
  133. package/dist/handlers/tools/index.d.ts +15 -20
  134. package/dist/handlers/tools/index.d.ts.map +1 -1
  135. package/dist/handlers/tools/index.js +117 -2909
  136. package/dist/handlers/tools/index.js.map +1 -1
  137. package/dist/handlers/tools/relationships.d.ts +8 -0
  138. package/dist/handlers/tools/relationships.d.ts.map +1 -0
  139. package/dist/handlers/tools/relationships.js +308 -0
  140. package/dist/handlers/tools/relationships.js.map +1 -0
  141. package/dist/handlers/tools/schemas.d.ts +108 -0
  142. package/dist/handlers/tools/schemas.d.ts.map +1 -0
  143. package/dist/handlers/tools/schemas.js +122 -0
  144. package/dist/handlers/tools/schemas.js.map +1 -0
  145. package/dist/handlers/tools/search.d.ts +8 -0
  146. package/dist/handlers/tools/search.d.ts.map +1 -0
  147. package/dist/handlers/tools/search.js +282 -0
  148. package/dist/handlers/tools/search.js.map +1 -0
  149. package/dist/handlers/tools/team.d.ts +11 -0
  150. package/dist/handlers/tools/team.d.ts.map +1 -0
  151. package/dist/handlers/tools/team.js +239 -0
  152. package/dist/handlers/tools/team.js.map +1 -0
  153. package/dist/server/McpServer.d.ts +4 -0
  154. package/dist/server/McpServer.d.ts.map +1 -1
  155. package/dist/server/McpServer.js +48 -297
  156. package/dist/server/McpServer.js.map +1 -1
  157. package/dist/server/Scheduler.d.ts +91 -0
  158. package/dist/server/Scheduler.d.ts.map +1 -0
  159. package/dist/server/Scheduler.js +201 -0
  160. package/dist/server/Scheduler.js.map +1 -0
  161. package/dist/transports/http.d.ts +66 -0
  162. package/dist/transports/http.d.ts.map +1 -0
  163. package/dist/transports/http.js +519 -0
  164. package/dist/transports/http.js.map +1 -0
  165. package/dist/types/entities.d.ts +101 -0
  166. package/dist/types/entities.d.ts.map +1 -0
  167. package/dist/types/entities.js +5 -0
  168. package/dist/types/entities.js.map +1 -0
  169. package/dist/types/filtering.d.ts +34 -0
  170. package/dist/types/filtering.d.ts.map +1 -0
  171. package/dist/types/filtering.js +5 -0
  172. package/dist/types/filtering.js.map +1 -0
  173. package/dist/types/github.d.ts +166 -0
  174. package/dist/types/github.d.ts.map +1 -0
  175. package/dist/types/github.js +5 -0
  176. package/dist/types/github.js.map +1 -0
  177. package/dist/types/index.d.ts +35 -292
  178. package/dist/types/index.d.ts.map +1 -1
  179. package/dist/types/index.js +2 -2
  180. package/dist/types/index.js.map +1 -1
  181. package/dist/utils/error-helpers.d.ts +37 -0
  182. package/dist/utils/error-helpers.d.ts.map +1 -0
  183. package/dist/utils/error-helpers.js +47 -0
  184. package/dist/utils/error-helpers.js.map +1 -0
  185. package/dist/utils/logger.d.ts.map +1 -1
  186. package/dist/utils/logger.js +6 -3
  187. package/dist/utils/logger.js.map +1 -1
  188. package/dist/utils/security-utils.d.ts +0 -21
  189. package/dist/utils/security-utils.d.ts.map +1 -1
  190. package/dist/utils/security-utils.js +0 -47
  191. package/dist/utils/security-utils.js.map +1 -1
  192. package/dist/vector/VectorSearchManager.d.ts.map +1 -1
  193. package/dist/vector/VectorSearchManager.js +9 -32
  194. package/dist/vector/VectorSearchManager.js.map +1 -1
  195. package/docker-compose.yml +11 -2
  196. package/hooks/README.md +107 -0
  197. package/hooks/cursor/hooks.json +10 -0
  198. package/hooks/cursor/memory-journal.mdc +22 -0
  199. package/hooks/cursor/session-end.sh +19 -0
  200. package/hooks/kilo-code/session-end-mode.json +11 -0
  201. package/hooks/kiro/session-end.md +13 -0
  202. package/mcp-config-example.json +1 -0
  203. package/package.json +11 -9
  204. package/playwright.config.ts +29 -0
  205. package/releases/v4.5.0.md +116 -0
  206. package/releases/v5.0.0.md +105 -0
  207. package/scripts/generate-server-instructions.ts +176 -0
  208. package/scripts/server-instructions-function-body.ts +77 -0
  209. package/server.json +3 -3
  210. package/src/cli.ts +45 -1
  211. package/src/constants/ServerInstructions.ts +133 -73
  212. package/src/constants/icons.ts +8 -7
  213. package/src/constants/server-instructions.md +268 -0
  214. package/src/database/SqliteAdapter.ts +358 -192
  215. package/src/database/schema.ts +125 -0
  216. package/src/filtering/ToolFilter.ts +13 -2
  217. package/src/github/GitHubIntegration.ts +1 -3
  218. package/src/handlers/prompts/github.ts +209 -0
  219. package/src/handlers/prompts/index.ts +10 -499
  220. package/src/handlers/prompts/workflow.ts +314 -0
  221. package/src/handlers/resources/core.ts +528 -0
  222. package/src/handlers/resources/github.ts +358 -0
  223. package/src/handlers/resources/graph.ts +254 -0
  224. package/src/handlers/resources/index.ts +23 -1570
  225. package/src/handlers/resources/shared.ts +103 -0
  226. package/src/handlers/resources/team.ts +133 -0
  227. package/src/handlers/resources/templates.ts +374 -0
  228. package/src/handlers/tools/admin.ts +285 -0
  229. package/src/handlers/tools/analytics.ts +301 -0
  230. package/src/handlers/tools/backup.ts +242 -0
  231. package/src/handlers/tools/core.ts +350 -0
  232. package/src/handlers/tools/export.ts +115 -0
  233. package/src/handlers/tools/github/helpers.ts +86 -0
  234. package/src/handlers/tools/github/insights-tools.ts +119 -0
  235. package/src/handlers/tools/github/issue-tools.ts +439 -0
  236. package/src/handlers/tools/github/kanban-tools.ts +134 -0
  237. package/src/handlers/tools/github/milestone-tools.ts +392 -0
  238. package/src/handlers/tools/github/mutation-tools.ts +17 -0
  239. package/src/handlers/tools/github/read-tools.ts +328 -0
  240. package/src/handlers/tools/github/schemas.ts +369 -0
  241. package/src/handlers/tools/github.ts +36 -0
  242. package/src/handlers/tools/index.ts +144 -3325
  243. package/src/handlers/tools/relationships.ts +358 -0
  244. package/src/handlers/tools/schemas.ts +132 -0
  245. package/src/handlers/tools/search.ts +343 -0
  246. package/src/handlers/tools/team.ts +273 -0
  247. package/src/server/McpServer.ts +63 -358
  248. package/src/server/Scheduler.ts +278 -0
  249. package/src/transports/http.ts +635 -0
  250. package/src/types/entities.ts +145 -0
  251. package/src/types/filtering.ts +54 -0
  252. package/src/types/github.ts +180 -0
  253. package/src/types/index.ts +67 -375
  254. package/src/utils/error-helpers.ts +52 -0
  255. package/src/utils/logger.ts +6 -3
  256. package/src/utils/security-utils.ts +0 -52
  257. package/src/vector/VectorSearchManager.ts +9 -33
  258. package/tests/constants/icons.test.ts +1 -2
  259. package/tests/constants/server-instructions.test.ts +30 -4
  260. package/tests/database/sqlite-adapter.test.ts +91 -7
  261. package/tests/e2e/auth.spec.ts +154 -0
  262. package/tests/e2e/health.spec.ts +63 -0
  263. package/tests/e2e/protocols.spec.ts +134 -0
  264. package/tests/e2e/resources.spec.ts +103 -0
  265. package/tests/e2e/scheduler.spec.ts +79 -0
  266. package/tests/e2e/security.spec.ts +91 -0
  267. package/tests/e2e/sessions.spec.ts +95 -0
  268. package/tests/e2e/stateless.spec.ts +121 -0
  269. package/tests/e2e/tools.spec.ts +111 -0
  270. package/tests/filtering/tool-filter.test.ts +46 -0
  271. package/tests/handlers/error-path-coverage.test.ts +324 -0
  272. package/tests/handlers/github-resource-handlers.test.ts +453 -0
  273. package/tests/handlers/github-tool-handlers.test.ts +899 -0
  274. package/tests/handlers/prompt-handler-coverage.test.ts +106 -0
  275. package/tests/handlers/prompt-handlers.test.ts +40 -0
  276. package/tests/handlers/resource-handler-coverage.test.ts +181 -0
  277. package/tests/handlers/resource-handlers.test.ts +33 -9
  278. package/tests/handlers/search-tool-handlers.test.ts +272 -0
  279. package/tests/handlers/targeted-gap-closure.test.ts +387 -0
  280. package/tests/handlers/team-resource-handlers.test.ts +156 -0
  281. package/tests/handlers/team-tool-handlers.test.ts +301 -0
  282. package/tests/handlers/tool-handler-coverage.test.ts +469 -0
  283. package/tests/handlers/tool-handlers.test.ts +2 -2
  284. package/tests/security/sql-injection.test.ts +3 -54
  285. package/tests/server/mcp-server.test.ts +503 -8
  286. package/tests/server/scheduler.test.ts +400 -0
  287. package/tests/transports/http-transport.test.ts +620 -0
  288. package/tests/vector/vector-search-manager.test.ts +60 -0
  289. package/vitest.config.ts +4 -1
  290. package/.memory-journal-team.db +0 -0
  291. package/.vscode/settings.json +0 -84
@@ -30,6 +30,7 @@ const {
30
30
  mockStdioTransport,
31
31
  mockListTags,
32
32
  mockHandlers,
33
+ mockSigintHandlers,
33
34
  } = vi.hoisted(() => ({
34
35
  mockRegisterTool: vi.fn(),
35
36
  mockRegisterResource: vi.fn(),
@@ -75,8 +76,9 @@ const {
75
76
  post: {} as Record<string, Function>,
76
77
  delete: {} as Record<string, Function>,
77
78
  all: {} as Record<string, Function>,
78
- use: {} as Record<string, Function>,
79
+ useMiddlewares: [] as Function[],
79
80
  },
81
+ mockSigintHandlers: [] as Function[],
80
82
  }))
81
83
 
82
84
  // ============================================================================
@@ -204,7 +206,7 @@ vi.mock('express', () => {
204
206
  const mockApp = {
205
207
  use: vi.fn().mockImplementation((...args: unknown[]) => {
206
208
  if (args.length === 1 && typeof args[0] === 'function') {
207
- mockHandlers.use['*'] = args[0] as () => void
209
+ mockHandlers.useMiddlewares.push(args[0] as Function)
208
210
  }
209
211
  }),
210
212
  get: vi.fn().mockImplementation((path: string, handler: unknown) => {
@@ -235,8 +237,13 @@ vi.mock('express', () => {
235
237
  }
236
238
  })
237
239
 
238
- // Suppress process.on/exit in tests
239
- vi.spyOn(process, 'on').mockImplementation(() => process)
240
+ // Capture process.on('SIGINT') handlers for testing
241
+ vi.spyOn(process, 'on').mockImplementation((event: string, handler: Function) => {
242
+ if (event === 'SIGINT') {
243
+ mockSigintHandlers.push(handler)
244
+ }
245
+ return process
246
+ })
240
247
  vi.spyOn(process, 'exit').mockImplementation((() => {}) as never)
241
248
 
242
249
  // ============================================================================
@@ -247,7 +254,24 @@ import { createServer, type ServerOptions } from '../../src/server/McpServer.js'
247
254
 
248
255
  describe('McpServer', () => {
249
256
  beforeEach(() => {
250
- vi.clearAllMocks()
257
+ // Reset call counts but preserve mock implementations
258
+ // (vi.clearAllMocks() would wipe .mockImplementation() on express mock)
259
+ mockRegisterTool.mockClear()
260
+ mockRegisterResource.mockClear()
261
+ mockRegisterPrompt.mockClear()
262
+ mockConnect.mockClear()
263
+ mockDbInitialize.mockClear()
264
+ mockDbClose.mockClear()
265
+ mockVectorInitialize.mockClear()
266
+ mockVectorRebuildIndex.mockClear()
267
+ mockCreateEntry.mockClear()
268
+ // Clear captured handler references
269
+ mockHandlers.get = {}
270
+ mockHandlers.post = {}
271
+ mockHandlers.delete = {}
272
+ mockHandlers.all = {}
273
+ mockHandlers.useMiddlewares.length = 0
274
+ mockSigintHandlers.length = 0
251
275
  })
252
276
 
253
277
  // ========================================================================
@@ -489,7 +513,7 @@ describe('McpServer', () => {
489
513
  expect(result.content[0]!.type).toBe('text')
490
514
  })
491
515
 
492
- it('should return error content when tool throws', async () => {
516
+ it('should return structured error content when tool throws', async () => {
493
517
  // Make createEntry throw
494
518
  mockCreateEntry.mockImplementationOnce(() => {
495
519
  throw new Error('Database error')
@@ -514,8 +538,17 @@ describe('McpServer', () => {
514
538
 
515
539
  const result = await handler({ content: 'Will fail' }, { _meta: {} })
516
540
 
517
- expect(result.isError).toBe(true)
518
- expect(result.content[0]!.text).toContain('Error')
541
+ // With deterministic error handling, errors are caught by the handler
542
+ // and returned as structured JSON (not as MCP isError)
543
+ expect(result.isError).toBeUndefined()
544
+ expect(result.content[0]!.type).toBe('text')
545
+
546
+ const parsed = JSON.parse(result.content[0]!.text) as {
547
+ success: boolean
548
+ error: string
549
+ }
550
+ expect(parsed.success).toBe(false)
551
+ expect(parsed.error).toContain('Database error')
519
552
  })
520
553
  })
521
554
 
@@ -671,5 +704,467 @@ describe('McpServer', () => {
671
704
  if (allHandler) await allHandler(mockReqGet, mockResOptions, nextFn)
672
705
  expect(nextFn).toHaveBeenCalled()
673
706
  })
707
+
708
+ it('should invoke security headers middleware', async () => {
709
+ const middlewareFns: Function[] = []
710
+ const { default: expressMod } = await import('express')
711
+ const app = expressMod()
712
+ // Capture all middleware registered via app.use()
713
+ ;(app.use as ReturnType<typeof vi.fn>).mockImplementation((...args: unknown[]) => {
714
+ if (args.length === 1 && typeof args[0] === 'function') {
715
+ middlewareFns.push(args[0] as Function)
716
+ }
717
+ })
718
+
719
+ await createServer({
720
+ transport: 'http',
721
+ dbPath: './test-server.db',
722
+ statelessHttp: true,
723
+ })
724
+
725
+ // The first middleware (after express.json) should set security headers
726
+ // Find a middleware that calls setHeader for security headers
727
+ let securityMiddlewareFound = false
728
+ for (const mw of middlewareFns) {
729
+ const mockRes = {
730
+ setHeader: vi.fn(),
731
+ status: vi.fn().mockReturnThis(),
732
+ end: vi.fn(),
733
+ }
734
+ const nextFn = vi.fn()
735
+ mw({}, mockRes, nextFn)
736
+ const calls = mockRes.setHeader.mock.calls as [string, string][]
737
+ const headerNames = calls.map((c) => c[0])
738
+ if (headerNames.includes('X-Content-Type-Options')) {
739
+ securityMiddlewareFound = true
740
+ expect(headerNames).toContain('X-Frame-Options')
741
+ expect(headerNames).toContain('Content-Security-Policy')
742
+ expect(headerNames).toContain('Cache-Control')
743
+ expect(headerNames).toContain('Referrer-Policy')
744
+ expect(nextFn).toHaveBeenCalled()
745
+ break
746
+ }
747
+ }
748
+ expect(securityMiddlewareFound).toBe(true)
749
+ })
750
+
751
+ it('should invoke CORS middleware and handle OPTIONS', async () => {
752
+ const middlewareFns: Function[] = []
753
+ const { default: expressMod } = await import('express')
754
+ const app = expressMod()
755
+ ;(app.use as ReturnType<typeof vi.fn>).mockImplementation((...args: unknown[]) => {
756
+ if (args.length === 1 && typeof args[0] === 'function') {
757
+ middlewareFns.push(args[0] as Function)
758
+ }
759
+ })
760
+
761
+ await createServer({
762
+ transport: 'http',
763
+ dbPath: './test-server.db',
764
+ statelessHttp: true,
765
+ corsOrigin: 'https://test.example.com',
766
+ })
767
+
768
+ // Find the CORS middleware (sets Access-Control-Allow-Origin)
769
+ let corsMiddlewareFound = false
770
+ for (const mw of middlewareFns) {
771
+ const mockRes = {
772
+ setHeader: vi.fn(),
773
+ status: vi.fn().mockReturnThis(),
774
+ end: vi.fn(),
775
+ }
776
+
777
+ // Test with OPTIONS request
778
+ const mockReqOptions = { method: 'OPTIONS' }
779
+ const noopNext = vi.fn()
780
+ mw(mockReqOptions, mockRes, noopNext)
781
+ const calls = mockRes.setHeader.mock.calls as [string, string][]
782
+ const headerNames = calls.map((c) => c[0])
783
+ if (headerNames.includes('Access-Control-Allow-Origin')) {
784
+ corsMiddlewareFound = true
785
+ expect(headerNames).toContain('Access-Control-Allow-Methods')
786
+ expect(headerNames).toContain('Access-Control-Allow-Headers')
787
+ expect(headerNames).toContain('Access-Control-Expose-Headers')
788
+ // OPTIONS should return 204
789
+ expect(mockRes.status).toHaveBeenCalledWith(204)
790
+ expect(mockRes.end).toHaveBeenCalled()
791
+ break
792
+ }
793
+ }
794
+ expect(corsMiddlewareFound).toBe(true)
795
+
796
+ // Test CORS middleware with non-OPTIONS request (calls next)
797
+ for (const mw of middlewareFns) {
798
+ const mockRes = {
799
+ setHeader: vi.fn(),
800
+ status: vi.fn().mockReturnThis(),
801
+ end: vi.fn(),
802
+ }
803
+ const nextFn = vi.fn()
804
+ mw({ method: 'POST' }, mockRes, nextFn)
805
+ const calls = mockRes.setHeader.mock.calls as [string, string][]
806
+ const headerNames = calls.map((c) => c[0])
807
+ if (headerNames.includes('Access-Control-Allow-Origin')) {
808
+ expect(nextFn).toHaveBeenCalled()
809
+ break
810
+ }
811
+ }
812
+ })
813
+
814
+ it('should handle stateful GET /mcp without session', async () => {
815
+ await createServer({
816
+ transport: 'http',
817
+ dbPath: './test-server.db',
818
+ statelessHttp: false,
819
+ })
820
+
821
+ const getHandler = mockHandlers.get['/mcp']
822
+ expect(getHandler).toBeDefined()
823
+
824
+ // No session ID
825
+ const mockReq = { headers: {} }
826
+ const mockRes = {
827
+ status: vi.fn().mockReturnThis(),
828
+ send: vi.fn(),
829
+ }
830
+
831
+ if (getHandler) await getHandler(mockReq, mockRes)
832
+ expect(mockRes.status).toHaveBeenCalledWith(400)
833
+ expect(mockRes.send).toHaveBeenCalledWith(
834
+ expect.stringContaining('Invalid or missing session ID')
835
+ )
836
+ })
837
+
838
+ it('should handle stateful DELETE /mcp without session', async () => {
839
+ await createServer({
840
+ transport: 'http',
841
+ dbPath: './test-server.db',
842
+ statelessHttp: false,
843
+ })
844
+
845
+ const deleteHandler = mockHandlers.delete['/mcp']
846
+ expect(deleteHandler).toBeDefined()
847
+
848
+ const mockReq = { headers: {} }
849
+ const mockRes = {
850
+ status: vi.fn().mockReturnThis(),
851
+ send: vi.fn(),
852
+ }
853
+
854
+ if (deleteHandler) await deleteHandler(mockReq, mockRes)
855
+ expect(mockRes.status).toHaveBeenCalledWith(400)
856
+ })
857
+
858
+ it('should handle stateful GET /mcp with invalid session', async () => {
859
+ await createServer({
860
+ transport: 'http',
861
+ dbPath: './test-server.db',
862
+ statelessHttp: false,
863
+ })
864
+
865
+ const getHandler = mockHandlers.get['/mcp']
866
+ const mockReq = { headers: { 'mcp-session-id': 'nonexistent-session' } }
867
+ const mockRes = {
868
+ status: vi.fn().mockReturnThis(),
869
+ send: vi.fn(),
870
+ }
871
+
872
+ if (getHandler) await getHandler(mockReq, mockRes)
873
+ expect(mockRes.status).toHaveBeenCalledWith(400)
874
+ })
875
+
876
+ it('should handle stateful DELETE /mcp with invalid session', async () => {
877
+ await createServer({
878
+ transport: 'http',
879
+ dbPath: './test-server.db',
880
+ statelessHttp: false,
881
+ })
882
+
883
+ const deleteHandler = mockHandlers.delete['/mcp']
884
+ const mockReq = { headers: { 'mcp-session-id': 'nonexistent-session' } }
885
+ const mockRes = {
886
+ status: vi.fn().mockReturnThis(),
887
+ send: vi.fn(),
888
+ }
889
+
890
+ if (deleteHandler) await deleteHandler(mockReq, mockRes)
891
+ expect(mockRes.status).toHaveBeenCalledWith(400)
892
+ })
893
+
894
+ it('should handle stateful POST /mcp with initialization request', async () => {
895
+ // Make isInitializeRequest return true for this test
896
+ const { isInitializeRequest: mockIsInit } =
897
+ await import('@modelcontextprotocol/sdk/types.js')
898
+ ;(mockIsInit as ReturnType<typeof vi.fn>).mockReturnValueOnce(true)
899
+
900
+ await createServer({
901
+ transport: 'http',
902
+ dbPath: './test-server.db',
903
+ statelessHttp: false,
904
+ })
905
+
906
+ const postHandler = mockHandlers.post['/mcp']
907
+ expect(postHandler).toBeDefined()
908
+
909
+ // Simulate initialization request (no session ID, isInitializeRequest returns true)
910
+ const mockReq = {
911
+ headers: {},
912
+ body: { jsonrpc: '2.0', id: 1, method: 'initialize' },
913
+ }
914
+ const mockRes = {
915
+ status: vi.fn().mockReturnThis(),
916
+ json: vi.fn(),
917
+ headersSent: false,
918
+ }
919
+
920
+ if (postHandler) {
921
+ await postHandler(mockReq, mockRes)
922
+ // Wait for async void handler to complete
923
+ await new Promise((r) => setTimeout(r, 50))
924
+ }
925
+
926
+ // The transport should have been created and connected
927
+ // (StreamableHTTPServerTransport mock handles the request)
928
+ expect(mockConnect).toHaveBeenCalled()
929
+ })
930
+
931
+ it('should handle stateful POST /mcp error with 500 response', async () => {
932
+ // Create a transport mock that throws
933
+ const StreamableTransportMod =
934
+ await import('@modelcontextprotocol/sdk/server/streamableHttp.js')
935
+ const OrigConstructor = StreamableTransportMod.StreamableHTTPServerTransport
936
+ const throwingConstructor = vi.fn().mockImplementation(() => {
937
+ return {
938
+ handleRequest: vi.fn().mockRejectedValue(new Error('Transport failure')),
939
+ close: vi.fn().mockResolvedValue(undefined),
940
+ sessionId: 'fail-session',
941
+ }
942
+ })
943
+ ;(StreamableTransportMod as Record<string, unknown>)['StreamableHTTPServerTransport'] =
944
+ throwingConstructor
945
+
946
+ await createServer({
947
+ transport: 'http',
948
+ dbPath: './test-server.db',
949
+ statelessHttp: false,
950
+ })
951
+
952
+ const postHandler = mockHandlers.post['/mcp']
953
+ expect(postHandler).toBeDefined()
954
+
955
+ // Request with existing session ID that throws during handleRequest
956
+ const mockReq = {
957
+ headers: { 'mcp-session-id': 'fail-session' },
958
+ body: { jsonrpc: '2.0', id: 1, method: 'test' },
959
+ }
960
+ const mockRes = {
961
+ status: vi.fn().mockReturnThis(),
962
+ json: vi.fn(),
963
+ headersSent: false,
964
+ }
965
+
966
+ if (postHandler) {
967
+ await postHandler(mockReq, mockRes)
968
+ await new Promise((r) => setTimeout(r, 50))
969
+ }
970
+
971
+ // Restore original
972
+ ;(StreamableTransportMod as Record<string, unknown>)['StreamableHTTPServerTransport'] =
973
+ OrigConstructor
974
+ })
975
+ })
976
+
977
+ // ========================================================================
978
+ // Scheduler
979
+ // ========================================================================
980
+
981
+ describe('createServer - scheduler', () => {
982
+ it('should warn and not start scheduler on stdio transport', async () => {
983
+ await createServer({
984
+ transport: 'stdio',
985
+ dbPath: './test-server.db',
986
+ scheduler: {
987
+ backupIntervalMinutes: 60,
988
+ vacuumIntervalMinutes: 120,
989
+ rebuildIndexIntervalMinutes: 180,
990
+ },
991
+ })
992
+
993
+ // Scheduler should NOT be started for stdio
994
+ // (no way to directly check but the code path is covered)
995
+ expect(mockDbInitialize).toHaveBeenCalled()
996
+ })
997
+
998
+ it('should start scheduler on HTTP transport', async () => {
999
+ await createServer({
1000
+ transport: 'http',
1001
+ dbPath: './test-server.db',
1002
+ statelessHttp: true,
1003
+ scheduler: {
1004
+ backupIntervalMinutes: 60,
1005
+ vacuumIntervalMinutes: 0,
1006
+ rebuildIndexIntervalMinutes: 0,
1007
+ },
1008
+ })
1009
+
1010
+ expect(mockDbInitialize).toHaveBeenCalled()
1011
+ })
1012
+
1013
+ it('should not create scheduler when all intervals are 0', async () => {
1014
+ await createServer({
1015
+ transport: 'http',
1016
+ dbPath: './test-server.db',
1017
+ scheduler: {
1018
+ backupIntervalMinutes: 0,
1019
+ vacuumIntervalMinutes: 0,
1020
+ rebuildIndexIntervalMinutes: 0,
1021
+ },
1022
+ })
1023
+
1024
+ expect(mockDbInitialize).toHaveBeenCalled()
1025
+ })
1026
+ })
1027
+
1028
+ // ========================================================================
1029
+ // Tool handler with outputSchema
1030
+ // ========================================================================
1031
+
1032
+ describe('tool handler structuredContent', () => {
1033
+ it('should return structuredContent for tools with outputSchema', async () => {
1034
+ await createServer({
1035
+ transport: 'stdio',
1036
+ dbPath: './test-server.db',
1037
+ })
1038
+
1039
+ // get_recent_entries has an outputSchema
1040
+ const calls = mockRegisterTool.mock.calls.filter(
1041
+ (call: unknown[]) => call[0] === 'get_recent_entries'
1042
+ ) as unknown[][]
1043
+
1044
+ expect(calls.length).toBe(1)
1045
+ const handler = calls[0]![2] as (
1046
+ args: Record<string, unknown>,
1047
+ extra: Record<string, unknown>
1048
+ ) => Promise<{
1049
+ content: { type: string; text: string }[]
1050
+ structuredContent?: Record<string, unknown>
1051
+ }>
1052
+
1053
+ const result = await handler({ limit: 5 }, { _meta: {} })
1054
+
1055
+ // Should have both content and structuredContent
1056
+ expect(result.content).toBeDefined()
1057
+ expect(result.structuredContent).toBeDefined()
1058
+ })
1059
+
1060
+ it('should pass progressToken to tool handler when provided', async () => {
1061
+ await createServer({
1062
+ transport: 'stdio',
1063
+ dbPath: './test-server.db',
1064
+ })
1065
+
1066
+ const calls = mockRegisterTool.mock.calls.filter(
1067
+ (call: unknown[]) => call[0] === 'create_entry'
1068
+ ) as unknown[][]
1069
+
1070
+ const handler = calls[0]![2] as (
1071
+ args: Record<string, unknown>,
1072
+ extra: Record<string, unknown>
1073
+ ) => Promise<unknown>
1074
+
1075
+ // Invoke with a progressToken in _meta
1076
+ const result = await handler(
1077
+ { content: 'Progress test' },
1078
+ { _meta: { progressToken: 'tok-123' } }
1079
+ )
1080
+
1081
+ expect(result).toBeDefined()
1082
+ })
1083
+ })
1084
+
1085
+ // ========================================================================
1086
+ // Environment-based tool filter
1087
+ // ========================================================================
1088
+
1089
+ describe('createServer - env tool filter', () => {
1090
+ it('should use MEMORY_JOURNAL_MCP_TOOL_FILTER env var when no explicit filter', async () => {
1091
+ process.env['MEMORY_JOURNAL_MCP_TOOL_FILTER'] = 'core'
1092
+
1093
+ await createServer({
1094
+ transport: 'stdio',
1095
+ dbPath: './test-server.db',
1096
+ })
1097
+
1098
+ // Tools should be filtered from env
1099
+ expect(mockRegisterTool).toHaveBeenCalled()
1100
+ const toolCount = mockRegisterTool.mock.calls.length
1101
+
1102
+ delete process.env['MEMORY_JOURNAL_MCP_TOOL_FILTER']
1103
+
1104
+ // Reset only the specific mock call counts we care about
1105
+ mockRegisterTool.mockClear()
1106
+ await createServer({
1107
+ transport: 'stdio',
1108
+ dbPath: './test-server.db',
1109
+ })
1110
+
1111
+ const unfilteredCount = mockRegisterTool.mock.calls.length
1112
+ // Env-filtered should have fewer tools than unfiltered
1113
+ expect(toolCount).toBeLessThan(unfilteredCount)
1114
+ })
1115
+ })
1116
+
1117
+ // ========================================================================
1118
+ // SIGINT shutdown handlers
1119
+ // ========================================================================
1120
+
1121
+ describe('createServer - shutdown handlers', () => {
1122
+ it('should register SIGINT handler for stdio transport', async () => {
1123
+ await createServer({
1124
+ transport: 'stdio',
1125
+ dbPath: './test-server.db',
1126
+ })
1127
+
1128
+ expect(mockSigintHandlers.length).toBe(1)
1129
+ })
1130
+
1131
+ it('should register SIGINT handler for stateless HTTP', async () => {
1132
+ await createServer({
1133
+ transport: 'http',
1134
+ dbPath: './test-server.db',
1135
+ statelessHttp: true,
1136
+ })
1137
+
1138
+ expect(mockSigintHandlers.length).toBe(1)
1139
+
1140
+ // Exercise the SIGINT handler
1141
+ const sigintHandler = mockSigintHandlers[0]
1142
+ if (sigintHandler) {
1143
+ sigintHandler()
1144
+ // Wait for async void
1145
+ await new Promise((r) => setTimeout(r, 50))
1146
+ }
1147
+
1148
+ expect(mockDbClose).toHaveBeenCalled()
1149
+ })
1150
+
1151
+ it('should register SIGINT handler for stateful HTTP', async () => {
1152
+ await createServer({
1153
+ transport: 'http',
1154
+ dbPath: './test-server.db',
1155
+ statelessHttp: false,
1156
+ })
1157
+
1158
+ expect(mockSigintHandlers.length).toBe(1)
1159
+
1160
+ // Exercise the SIGINT handler
1161
+ const sigintHandler = mockSigintHandlers[0]
1162
+ if (sigintHandler) {
1163
+ sigintHandler()
1164
+ await new Promise((r) => setTimeout(r, 50))
1165
+ }
1166
+
1167
+ expect(mockDbClose).toHaveBeenCalled()
1168
+ })
674
1169
  })
675
1170
  })