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
@@ -0,0 +1,79 @@
1
+ /**
2
+ * E2E Tests: Scheduler Activation
3
+ *
4
+ * Verifies that the automated scheduler is active and visible
5
+ * in the memory://health resource when the server is started
6
+ * with scheduler flags (--backup-interval, --vacuum-interval, etc.)
7
+ */
8
+
9
+ import { test, expect } from '@playwright/test'
10
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
11
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
12
+
13
+ test.describe('Scheduler Activation (HTTP Only)', () => {
14
+ let client: Client
15
+
16
+ test.beforeAll(async () => {
17
+ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3100/mcp'))
18
+ client = new Client(
19
+ { name: 'playwright-scheduler-test', version: '1.0.0' },
20
+ { capabilities: {} }
21
+ )
22
+ await client.connect(transport)
23
+ })
24
+
25
+ test.afterAll(async () => {
26
+ await client.close()
27
+ })
28
+
29
+ test('memory://health should show scheduler.active as true', async () => {
30
+ const response = await client.readResource({ uri: 'memory://health' })
31
+
32
+ expect(response.contents).toBeDefined()
33
+ expect(response.contents.length).toBeGreaterThan(0)
34
+
35
+ const text = response.contents[0]!.text as string
36
+ const health = JSON.parse(text)
37
+
38
+ expect(health).toHaveProperty('scheduler')
39
+ expect(health.scheduler).toHaveProperty('active', true)
40
+ })
41
+
42
+ test('memory://health should show 3 scheduler jobs', async () => {
43
+ const response = await client.readResource({ uri: 'memory://health' })
44
+ const health = JSON.parse(response.contents[0]!.text as string)
45
+
46
+ expect(health.scheduler).toHaveProperty('jobs')
47
+ expect(Array.isArray(health.scheduler.jobs)).toBe(true)
48
+ expect(health.scheduler.jobs.length).toBe(3)
49
+
50
+ const jobNames = health.scheduler.jobs.map((j: { name: string }) => j.name)
51
+ expect(jobNames).toContain('backup')
52
+ expect(jobNames).toContain('vacuum')
53
+ expect(jobNames).toContain('rebuild-index')
54
+ })
55
+
56
+ test('scheduler jobs should have nextRun timestamps', async () => {
57
+ const response = await client.readResource({ uri: 'memory://health' })
58
+ const health = JSON.parse(response.contents[0]!.text as string)
59
+
60
+ for (const job of health.scheduler.jobs) {
61
+ expect(job).toHaveProperty('nextRun')
62
+ // nextRun should be a valid ISO 8601 timestamp
63
+ const nextRun = new Date(job.nextRun)
64
+ expect(nextRun.toISOString()).toBe(job.nextRun)
65
+ // nextRun should be in the future (or very close to now)
66
+ expect(nextRun.getTime()).toBeGreaterThan(Date.now() - 60000)
67
+ }
68
+ })
69
+
70
+ test('scheduler jobs should have run count fields', async () => {
71
+ const response = await client.readResource({ uri: 'memory://health' })
72
+ const health = JSON.parse(response.contents[0]!.text as string)
73
+
74
+ for (const job of health.scheduler.jobs) {
75
+ expect(job).toHaveProperty('runCount')
76
+ expect(typeof job.runCount).toBe('number')
77
+ }
78
+ })
79
+ })
@@ -0,0 +1,91 @@
1
+ /**
2
+ * E2E Tests: Security & Limits
3
+ *
4
+ * Tests HTTP transport security hardening: security headers,
5
+ * body size limits, 404 handler, CORS, and HSTS.
6
+ */
7
+
8
+ import { test, expect } from '@playwright/test'
9
+
10
+ test.describe('HTTP Transport Security & Limits', () => {
11
+ test('should return 404 Not Found for unknown endpoints', async ({ request }) => {
12
+ const response = await request.get('/non-existent-path')
13
+ expect(response.status()).toBe(404)
14
+
15
+ const body = await response.json()
16
+ expect(body).toHaveProperty('error', 'Not found')
17
+ })
18
+
19
+ test('should return 413 Payload Too Large for excessive POST bodies', async ({ request }) => {
20
+ // Generate a payload over 1 MB (1,048,576 bytes)
21
+ const bulkyData = 'A'.repeat(1024 * 1025) // ~1.025 MB string
22
+
23
+ const response = await request.post('/mcp', {
24
+ headers: {
25
+ Accept: 'application/json, text/event-stream',
26
+ },
27
+ data: {
28
+ jsonrpc: '2.0',
29
+ id: 1,
30
+ method: 'initialize',
31
+ params: { testData: bulkyData },
32
+ },
33
+ })
34
+
35
+ expect(response.status()).toBe(413)
36
+ })
37
+
38
+ test('should inject security headers on responses', async ({ request }) => {
39
+ const response = await request.get('/health')
40
+ expect(response.status()).toBe(200)
41
+
42
+ const headers = response.headers()
43
+ expect(headers['x-content-type-options']).toBe('nosniff')
44
+ expect(headers['x-frame-options']).toBe('DENY')
45
+ expect(headers['cache-control']).toBe('no-store')
46
+ expect(headers['content-security-policy']).toBe(
47
+ "default-src 'none'; frame-ancestors 'none'"
48
+ )
49
+ expect(headers['referrer-policy']).toBe('no-referrer')
50
+ expect(headers['permissions-policy']).toBe('camera=(), microphone=(), geolocation=()')
51
+ })
52
+
53
+ test('should respond with 204 to CORS preflight OPTIONS requests', async ({ request }) => {
54
+ const response = await request.fetch('/mcp', {
55
+ method: 'OPTIONS',
56
+ })
57
+
58
+ expect(response.status()).toBe(204)
59
+ })
60
+
61
+ test('should include CORS headers on responses', async ({ request }) => {
62
+ const response = await request.get('/health')
63
+ const headers = response.headers()
64
+
65
+ // Default CORS origin is * (configurable via --cors-origin)
66
+ expect(headers['access-control-allow-origin']).toBe('*')
67
+ expect(headers['access-control-allow-methods']).toContain('POST')
68
+ expect(headers['access-control-allow-methods']).toContain('GET')
69
+ expect(headers['access-control-expose-headers']).toContain('mcp-session-id')
70
+ })
71
+
72
+ test('should include HSTS header when X-Forwarded-Proto is https', async ({ request }) => {
73
+ const response = await request.get('/health', {
74
+ headers: {
75
+ 'X-Forwarded-Proto': 'https',
76
+ },
77
+ })
78
+
79
+ expect(response.status()).toBe(200)
80
+ const headers = response.headers()
81
+ expect(headers['strict-transport-security']).toBe('max-age=31536000; includeSubDomains')
82
+ })
83
+
84
+ test('should NOT include HSTS header without X-Forwarded-Proto', async ({ request }) => {
85
+ const response = await request.get('/health')
86
+ expect(response.status()).toBe(200)
87
+
88
+ const headers = response.headers()
89
+ expect(headers['strict-transport-security']).toBeUndefined()
90
+ })
91
+ })
@@ -0,0 +1,95 @@
1
+ /**
2
+ * E2E Tests: Session Lifecycle
3
+ *
4
+ * Tests the full Streamable HTTP session lifecycle: initialization,
5
+ * tool calls with session ID, session termination (DELETE /mcp),
6
+ * and rejection of stale/invalid sessions.
7
+ */
8
+
9
+ import { test, expect } from '@playwright/test'
10
+
11
+ test.describe.configure({ mode: 'serial' })
12
+
13
+ test.describe('Session Lifecycle', () => {
14
+ let sessionId: string
15
+
16
+ test('should initialize a session and return session ID', async ({ request }) => {
17
+ const response = await request.post('/mcp', {
18
+ headers: {
19
+ Accept: 'application/json, text/event-stream',
20
+ },
21
+ data: {
22
+ jsonrpc: '2.0',
23
+ id: 1,
24
+ method: 'initialize',
25
+ params: {
26
+ protocolVersion: '2025-03-26',
27
+ capabilities: {},
28
+ clientInfo: {
29
+ name: 'playwright-session-test',
30
+ version: '1.0.0',
31
+ },
32
+ },
33
+ },
34
+ })
35
+
36
+ expect(response.status()).toBe(200)
37
+ sessionId = response.headers()['mcp-session-id']!
38
+ expect(sessionId).toBeDefined()
39
+ expect(sessionId.length).toBeGreaterThan(0)
40
+ })
41
+
42
+ test('should accept requests with valid session ID', async ({ request }) => {
43
+ // Send initialized notification (required by MCP protocol after init)
44
+ const response = await request.post('/mcp', {
45
+ headers: {
46
+ Accept: 'application/json, text/event-stream',
47
+ 'mcp-session-id': sessionId,
48
+ },
49
+ data: {
50
+ jsonrpc: '2.0',
51
+ method: 'notifications/initialized',
52
+ },
53
+ })
54
+
55
+ // Notifications return 202 (accepted, no response body) or 200
56
+ expect([200, 202, 204]).toContain(response.status())
57
+ })
58
+
59
+ test('should allow tool calls with valid session ID', async ({ request }) => {
60
+ const response = await request.post('/mcp', {
61
+ headers: {
62
+ Accept: 'application/json, text/event-stream',
63
+ 'mcp-session-id': sessionId,
64
+ },
65
+ data: {
66
+ jsonrpc: '2.0',
67
+ id: 2,
68
+ method: 'tools/list',
69
+ },
70
+ })
71
+
72
+ expect(response.status()).toBe(200)
73
+ })
74
+
75
+ test('GET /mcp (SSE) should reject without session ID', async ({ request }) => {
76
+ const response = await request.get('/mcp')
77
+ expect(response.status()).toBe(400)
78
+ })
79
+
80
+ test('DELETE /mcp should reject without session ID', async ({ request }) => {
81
+ const response = await request.delete('/mcp')
82
+ expect(response.status()).toBe(400)
83
+ })
84
+
85
+ test('DELETE /mcp should terminate a valid session', async ({ request }) => {
86
+ const response = await request.delete('/mcp', {
87
+ headers: {
88
+ 'mcp-session-id': sessionId,
89
+ },
90
+ })
91
+
92
+ // DELETE returns 200 or 204
93
+ expect([200, 204]).toContain(response.status())
94
+ })
95
+ })
@@ -0,0 +1,121 @@
1
+ /**
2
+ * E2E Tests: Stateless HTTP Mode
3
+ *
4
+ * Tests the --stateless mode behavior. Uses a test-local server
5
+ * on port 3102 to avoid conflicting with the main webServer.
6
+ */
7
+
8
+ import { test, expect } from '@playwright/test'
9
+ import { type ChildProcess, spawn } from 'node:child_process'
10
+ import { setTimeout as delay } from 'node:timers/promises'
11
+
12
+ const STATELESS_PORT = 3102
13
+ const STATELESS_BASE = `http://localhost:${STATELESS_PORT}`
14
+
15
+ let serverProcess: ChildProcess | null = null
16
+
17
+ async function startStatelessServer(): Promise<void> {
18
+ serverProcess = spawn(
19
+ 'node',
20
+ [
21
+ 'dist/cli.js',
22
+ '--transport',
23
+ 'http',
24
+ '--port',
25
+ String(STATELESS_PORT),
26
+ '--db',
27
+ './test-e2e-stateless.db',
28
+ '--stateless',
29
+ ],
30
+ {
31
+ cwd: process.cwd(),
32
+ stdio: 'pipe',
33
+ }
34
+ )
35
+
36
+ const maxAttempts = 30
37
+ for (let i = 0; i < maxAttempts; i++) {
38
+ try {
39
+ const res = await fetch(`${STATELESS_BASE}/health`)
40
+ if (res.ok) return
41
+ } catch {
42
+ // Server not ready yet
43
+ }
44
+ await delay(500)
45
+ }
46
+ throw new Error('Stateless server did not start within timeout')
47
+ }
48
+
49
+ function stopStatelessServer(): void {
50
+ if (serverProcess) {
51
+ serverProcess.kill('SIGTERM')
52
+ serverProcess = null
53
+ }
54
+ }
55
+
56
+ test.describe('Stateless HTTP Mode', () => {
57
+ test.beforeAll(async () => {
58
+ await startStatelessServer()
59
+ })
60
+
61
+ test.afterAll(() => {
62
+ stopStatelessServer()
63
+ })
64
+
65
+ test('POST /mcp should accept requests without session ID (stateless)', async () => {
66
+ const response = await fetch(`${STATELESS_BASE}/mcp`, {
67
+ method: 'POST',
68
+ headers: {
69
+ 'Content-Type': 'application/json',
70
+ Accept: 'application/json, text/event-stream',
71
+ },
72
+ body: JSON.stringify({
73
+ jsonrpc: '2.0',
74
+ id: 1,
75
+ method: 'initialize',
76
+ params: {
77
+ protocolVersion: '2025-03-26',
78
+ capabilities: {},
79
+ clientInfo: { name: 'stateless-test', version: '1.0' },
80
+ },
81
+ }),
82
+ })
83
+
84
+ expect(response.status).toBe(200)
85
+ })
86
+
87
+ test('GET /mcp should return 405 (SSE not available in stateless)', async () => {
88
+ const response = await fetch(`${STATELESS_BASE}/mcp`)
89
+
90
+ expect(response.status).toBe(405)
91
+ const body = await response.json()
92
+ expect(body.error).toHaveProperty(
93
+ 'message',
94
+ 'SSE streaming not available in stateless mode'
95
+ )
96
+ })
97
+
98
+ test('DELETE /mcp should return 204 (no-op in stateless)', async () => {
99
+ const response = await fetch(`${STATELESS_BASE}/mcp`, {
100
+ method: 'DELETE',
101
+ })
102
+
103
+ expect(response.status).toBe(204)
104
+ })
105
+
106
+ test('GET /sse should return 404 (legacy SSE not available in stateless)', async () => {
107
+ const response = await fetch(`${STATELESS_BASE}/sse`)
108
+
109
+ expect(response.status).toBe(404)
110
+ const body = await response.json()
111
+ expect(body).toHaveProperty('error', 'Not found')
112
+ })
113
+
114
+ test('/health should still work in stateless mode', async () => {
115
+ const response = await fetch(`${STATELESS_BASE}/health`)
116
+ expect(response.status).toBe(200)
117
+
118
+ const body = await response.json()
119
+ expect(body).toHaveProperty('status', 'healthy')
120
+ })
121
+ })
@@ -0,0 +1,111 @@
1
+ /**
2
+ * E2E Tests: MCP Tool Execution via SDK Client
3
+ *
4
+ * Uses the official @modelcontextprotocol/sdk client to connect
5
+ * via Streamable HTTP transport and execute tools end-to-end.
6
+ */
7
+
8
+ import { test, expect } from '@playwright/test'
9
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
10
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
11
+
12
+ test.describe.configure({ mode: 'serial' })
13
+
14
+ test.describe('E2E Tool Execution (via MCP SDK Client)', () => {
15
+ let client: Client
16
+
17
+ test.beforeAll(async () => {
18
+ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3100/mcp'))
19
+ client = new Client(
20
+ { name: 'playwright-tool-test', version: '1.0.0' },
21
+ { capabilities: {} }
22
+ )
23
+ await client.connect(transport)
24
+ })
25
+
26
+ test.afterAll(async () => {
27
+ await client.close()
28
+ })
29
+
30
+ test('should list available tools', async () => {
31
+ const listResponse = await client.listTools()
32
+
33
+ expect(listResponse.tools).toBeDefined()
34
+ expect(Array.isArray(listResponse.tools)).toBe(true)
35
+ expect(listResponse.tools.length).toBeGreaterThan(0)
36
+
37
+ const toolNames = listResponse.tools.map((t) => t.name)
38
+ expect(toolNames).toContain('create_entry')
39
+ expect(toolNames).toContain('search_entries')
40
+ expect(toolNames).toContain('test_simple')
41
+ expect(toolNames).toContain('get_recent_entries')
42
+ })
43
+
44
+ test('should execute test_simple tool', async () => {
45
+ const response = await client.callTool({
46
+ name: 'test_simple',
47
+ arguments: { message: 'Playwright E2E' },
48
+ })
49
+
50
+ expect(response.isError).toBeUndefined()
51
+ expect(Array.isArray(response.content)).toBe(true)
52
+ expect(response.content.length).toBeGreaterThan(0)
53
+ expect(response.content[0]).toHaveProperty('type', 'text')
54
+
55
+ const text = (response.content[0] as { type: string; text: string }).text
56
+ expect(text).toContain('Playwright E2E')
57
+ })
58
+
59
+ test('should execute create_entry_minimal and get structured response', async () => {
60
+ const response = await client.callTool({
61
+ name: 'create_entry_minimal',
62
+ arguments: { content: 'Playwright e2e test entry' },
63
+ })
64
+
65
+ expect(response.isError).toBeUndefined()
66
+ expect(Array.isArray(response.content)).toBe(true)
67
+ expect(response.content.length).toBeGreaterThan(0)
68
+
69
+ const text = (response.content[0] as { type: string; text: string }).text
70
+ const parsed = JSON.parse(text)
71
+ expect(parsed).toHaveProperty('success', true)
72
+ expect(parsed).toHaveProperty('entry')
73
+ expect(parsed.entry).toHaveProperty('id')
74
+ })
75
+
76
+ test('should execute get_recent_entries and return entries array', async () => {
77
+ const response = await client.callTool({
78
+ name: 'get_recent_entries',
79
+ arguments: { limit: 3 },
80
+ })
81
+
82
+ expect(response.isError).toBeUndefined()
83
+ expect(Array.isArray(response.content)).toBe(true)
84
+ expect(response.content.length).toBeGreaterThan(0)
85
+
86
+ const text = (response.content[0] as { type: string; text: string }).text
87
+ const parsed = JSON.parse(text)
88
+ expect(parsed).toHaveProperty('entries')
89
+ expect(Array.isArray(parsed.entries)).toBe(true)
90
+ })
91
+
92
+ test('should return structured error for invalid entry_type', async () => {
93
+ const response = await client.callTool({
94
+ name: 'create_entry',
95
+ arguments: {
96
+ content: 'test validation',
97
+ entry_type: 'invalid_type',
98
+ },
99
+ })
100
+
101
+ // Should NOT be a raw MCP error — should be structured handler error
102
+ expect(response.isError).toBeUndefined()
103
+ expect(Array.isArray(response.content)).toBe(true)
104
+
105
+ const text = (response.content[0] as { type: string; text: string }).text
106
+ const parsed = JSON.parse(text)
107
+ expect(parsed).toHaveProperty('success', false)
108
+ expect(parsed).toHaveProperty('error')
109
+ expect(parsed.error).toContain('entry_type')
110
+ })
111
+ })
@@ -264,3 +264,49 @@ describe('getFilterSummary', () => {
264
264
  expect(summary.length).toBeGreaterThan(0)
265
265
  })
266
266
  })
267
+
268
+ // ============================================================================
269
+ // parseToolFilter edge cases
270
+ // ============================================================================
271
+
272
+ describe('parseToolFilter edge cases', () => {
273
+ it('should handle blacklist-first mode (-admin = all except admin)', () => {
274
+ const config = parseToolFilter('-admin')
275
+ // Should have all tools EXCEPT admin tools
276
+ expect(config.enabledTools.has('create_entry')).toBe(true)
277
+ expect(config.enabledTools.has('search_entries')).toBe(true)
278
+ expect(config.enabledTools.has('backup_journal')).toBe(true)
279
+ // Admin tools should be excluded
280
+ expect(config.enabledTools.has('delete_entry')).toBe(false)
281
+ })
282
+
283
+ it('should handle single tool name whitelist', () => {
284
+ const config = parseToolFilter('create_entry')
285
+ expect(config.enabledTools.has('create_entry')).toBe(true)
286
+ expect(config.enabledTools.size).toBe(1)
287
+ })
288
+
289
+ it('should handle meta-group in non-first position', () => {
290
+ const config = parseToolFilter('backup,starter')
291
+ // starter = core + search, plus backup group
292
+ expect(config.enabledTools.has('create_entry')).toBe(true)
293
+ expect(config.enabledTools.has('search_entries')).toBe(true)
294
+ expect(config.enabledTools.has('backup_journal')).toBe(true)
295
+ })
296
+
297
+ it('should handle combined meta-group with tool exclusion', () => {
298
+ const config = parseToolFilter('starter,-create_entry_minimal')
299
+ expect(config.enabledTools.has('create_entry')).toBe(true)
300
+ expect(config.enabledTools.has('create_entry_minimal')).toBe(false)
301
+ })
302
+
303
+ it('should handle multiple exclusions', () => {
304
+ const config = parseToolFilter('full,-admin,-backup,-github')
305
+ expect(config.enabledTools.has('create_entry')).toBe(true)
306
+ expect(config.enabledTools.has('search_entries')).toBe(true)
307
+ // Excluded groups
308
+ expect(config.enabledTools.has('delete_entry')).toBe(false)
309
+ expect(config.enabledTools.has('backup_journal')).toBe(false)
310
+ expect(config.enabledTools.has('get_github_issues')).toBe(false)
311
+ })
312
+ })