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,635 @@
1
+ /* eslint-disable @typescript-eslint/no-deprecated -- Intentional: SSEServerTransport provides backward compatibility for MCP 2024-11-05 clients */
2
+ /**
3
+ * Memory Journal MCP Server - HTTP Transport
4
+ *
5
+ * Dual-protocol HTTP transport:
6
+ * - `/mcp` — Streamable HTTP transport (MCP 2025-03-26)
7
+ * - `/sse` + `/messages` — Legacy SSE transport (MCP 2024-11-05)
8
+ *
9
+ * Modes:
10
+ * - Stateful (default): Multi-session management with SSE streaming
11
+ * - Stateless (opt-in): Lightweight serverless-compatible mode
12
+ *
13
+ * Security: headers, CORS, rate limiting, body size enforcement.
14
+ */
15
+
16
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
17
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'
18
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
19
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
20
+ import { randomUUID, timingSafeEqual } from 'node:crypto'
21
+ import type { IncomingMessage, ServerResponse } from 'node:http'
22
+ import express from 'express'
23
+ import type { Express, Request, Response } from 'express'
24
+ import rateLimit from 'express-rate-limit'
25
+ import { logger } from '../utils/logger.js'
26
+ import type { Scheduler } from '../server/Scheduler.js'
27
+ import pkg from '../../package.json' with { type: 'json' }
28
+
29
+ /** Session timeout for stateful HTTP mode (30 minutes) */
30
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000
31
+
32
+ /** Session timeout sweep interval (5 minutes) */
33
+ const SESSION_SWEEP_INTERVAL_MS = 5 * 60 * 1000
34
+
35
+ /**
36
+ * HTTP transport configuration
37
+ */
38
+ export interface HttpTransportConfig {
39
+ port: number
40
+ host: string
41
+ corsOrigin: string
42
+ stateless: boolean
43
+ authToken?: string
44
+ }
45
+
46
+ /**
47
+ * HTTP Transport for Memory Journal MCP Server
48
+ *
49
+ * Supports two transport protocols simultaneously:
50
+ * 1. Streamable HTTP (MCP 2025-03-26) via `/mcp` — preferred for modern clients
51
+ * 2. Legacy SSE (MCP 2024-11-05) via `/sse` + `/messages` — backward compatibility
52
+ */
53
+ export class HttpTransport {
54
+ private readonly app: Express
55
+ private readonly config: HttpTransportConfig
56
+ private readonly transports = new Map<string, StreamableHTTPServerTransport>()
57
+ private readonly sseTransports = new Map<string, SSEServerTransport>()
58
+ private readonly sessionLastActivity = new Map<string, number>()
59
+ private httpServer: ReturnType<Express['listen']> | null = null
60
+ private sessionSweepTimer: ReturnType<typeof setInterval> | null = null
61
+
62
+ constructor(config: HttpTransportConfig) {
63
+ this.config = config
64
+ this.app = express()
65
+ }
66
+
67
+ /**
68
+ * Initialize and start the HTTP transport
69
+ */
70
+ async start(server: McpServer, scheduler: Scheduler | null): Promise<void> {
71
+ const { port, host, corsOrigin, authToken } = this.config
72
+
73
+ if (corsOrigin === '*') {
74
+ logger.warning(
75
+ 'CORS origin is set to "*" (all origins). ' +
76
+ 'Set --cors-origin or MCP_CORS_ORIGIN for production deployments.',
77
+ { module: 'HTTP' }
78
+ )
79
+ }
80
+
81
+ if (!authToken) {
82
+ logger.warning(
83
+ 'No authentication configured for HTTP transport. ' +
84
+ 'Set --auth-token or MCP_AUTH_TOKEN for production deployments.',
85
+ { module: 'HTTP' }
86
+ )
87
+ }
88
+
89
+ // Security headers middleware
90
+ this.app.use((req: Request, res: Response, next: () => void) => {
91
+ res.setHeader('X-Content-Type-Options', 'nosniff')
92
+ res.setHeader('X-Frame-Options', 'DENY')
93
+ res.setHeader('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'")
94
+ res.setHeader('Cache-Control', 'no-store')
95
+ res.setHeader('Referrer-Policy', 'no-referrer')
96
+ res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
97
+ // HSTS — only emit when behind a TLS-terminating reverse proxy
98
+ if (req.headers?.['x-forwarded-proto'] === 'https') {
99
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
100
+ }
101
+ next()
102
+ })
103
+
104
+ // CORS middleware
105
+ this.app.use((req: Request, res: Response, next: () => void) => {
106
+ res.setHeader('Access-Control-Allow-Origin', corsOrigin)
107
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
108
+ res.setHeader(
109
+ 'Access-Control-Allow-Headers',
110
+ 'Content-Type, Accept, Authorization, mcp-session-id, Last-Event-ID, mcp-protocol-version'
111
+ )
112
+ res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id')
113
+
114
+ if (req.method === 'OPTIONS') {
115
+ res.status(204).end()
116
+ return
117
+ }
118
+
119
+ next()
120
+ })
121
+
122
+ // JSON body parser with size limit (DoS prevention)
123
+ this.app.use(express.json({ limit: '1mb' }))
124
+
125
+ // Rate limiting (100 requests/minute per IP)
126
+ const limiter = rateLimit({
127
+ windowMs: 60 * 1000,
128
+ limit: 100,
129
+ standardHeaders: 'draft-8',
130
+ legacyHeaders: false,
131
+ message: { error: 'Too many requests, please try again later' },
132
+ })
133
+ this.app.use(limiter)
134
+ logger.info('Rate limiting enabled: 100 requests/minute per IP', {
135
+ module: 'HTTP',
136
+ })
137
+
138
+ // Bearer token authentication (when configured)
139
+ if (authToken) {
140
+ this.app.use((req: Request, res: Response, next: () => void) => {
141
+ if (req.path === '/health') {
142
+ next()
143
+ return
144
+ }
145
+
146
+ const header = req.headers.authorization
147
+ const expected = Buffer.from(`Bearer ${authToken}`)
148
+ const received = Buffer.from(header ?? '')
149
+ if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
150
+ res.status(401).json({ error: 'Unauthorized' })
151
+ return
152
+ }
153
+ next()
154
+ })
155
+ logger.info('Bearer token authentication enabled', { module: 'HTTP' })
156
+ }
157
+
158
+ // Health check endpoint (before /mcp routes)
159
+ this.app.get('/health', (_req: Request, res: Response): void => {
160
+ res.status(200).json({
161
+ status: 'healthy',
162
+ timestamp: new Date().toISOString(),
163
+ })
164
+ })
165
+
166
+ // Root info endpoint
167
+ this.app.get('/', (_req: Request, res: Response): void => {
168
+ res.status(200).json({
169
+ name: 'memory-journal-mcp',
170
+ version: pkg.version,
171
+ description: 'Project context management for AI-assisted development',
172
+ endpoints: {
173
+ 'POST /mcp': 'JSON-RPC requests (Streamable HTTP, MCP 2025-03-26)',
174
+ 'GET /mcp': 'SSE stream for server-to-client notifications',
175
+ 'DELETE /mcp': 'Session termination',
176
+ 'GET /sse': 'Legacy SSE connection (MCP 2024-11-05)',
177
+ 'POST /messages': 'Legacy SSE message endpoint',
178
+ 'GET /health': 'Health check',
179
+ },
180
+ documentation: 'https://github.com/neverinfamous/memory-journal-mcp',
181
+ })
182
+ })
183
+
184
+ // OPTIONS handler for /mcp — MUST be before other /mcp routes
185
+ this.app.all('/mcp', (req: Request, res: Response, next: () => void) => {
186
+ if (req.method === 'OPTIONS') {
187
+ res.status(204).end()
188
+ return
189
+ }
190
+ next()
191
+ })
192
+
193
+ if (this.config.stateless) {
194
+ await this.setupStateless(server)
195
+ } else {
196
+ this.setupStateful(server)
197
+ this.setupLegacySSE(server)
198
+ }
199
+
200
+ // 404 handler — must be after all routes
201
+ this.app.use((_req: Request, res: Response): void => {
202
+ res.status(404).json({ error: 'Not found' })
203
+ })
204
+
205
+ // Start HTTP server
206
+ this.httpServer = this.app.listen(port, host, () => {
207
+ logger.info(
208
+ `MCP server started on HTTP (${this.config.stateless ? 'stateless' : 'stateful'})`,
209
+ {
210
+ module: 'HTTP',
211
+ port,
212
+ host,
213
+ endpoint: `http://${host}:${port}/mcp`,
214
+ }
215
+ )
216
+ })
217
+
218
+ // Start scheduler after HTTP server is listening
219
+ scheduler?.start()
220
+
221
+ this.httpServer.on('close', () => {
222
+ logger.info('HTTP server closed', { module: 'HTTP' })
223
+ })
224
+ }
225
+
226
+ /**
227
+ * Graceful shutdown
228
+ */
229
+ async stop(scheduler: Scheduler | null): Promise<void> {
230
+ logger.info('Shutting down HTTP server...', { module: 'HTTP' })
231
+
232
+ scheduler?.stop()
233
+
234
+ // Close all Streamable HTTP transports
235
+ for (const [sessionId, transport] of this.transports) {
236
+ try {
237
+ logger.debug('Closing Streamable HTTP transport', {
238
+ module: 'HTTP',
239
+ sessionId,
240
+ })
241
+ await transport.close()
242
+ } catch (error) {
243
+ logger.error('Error closing transport', {
244
+ module: 'HTTP',
245
+ sessionId,
246
+ error: error instanceof Error ? error.message : String(error),
247
+ })
248
+ }
249
+ }
250
+ this.transports.clear()
251
+
252
+ // Close all Legacy SSE transports
253
+ for (const [sessionId, transport] of this.sseTransports) {
254
+ try {
255
+ logger.debug('Closing Legacy SSE transport', {
256
+ module: 'HTTP',
257
+ sessionId,
258
+ })
259
+ await transport.close()
260
+ } catch (error) {
261
+ logger.error('Error closing SSE transport', {
262
+ module: 'HTTP',
263
+ sessionId,
264
+ error: error instanceof Error ? error.message : String(error),
265
+ })
266
+ }
267
+ }
268
+ this.sseTransports.clear()
269
+
270
+ this.sessionLastActivity.clear()
271
+ if (this.sessionSweepTimer !== null) {
272
+ clearInterval(this.sessionSweepTimer)
273
+ }
274
+
275
+ if (this.httpServer !== null) {
276
+ this.httpServer.close()
277
+ }
278
+
279
+ logger.info('Shutdown complete', { module: 'HTTP' })
280
+ }
281
+
282
+ // =========================================================================
283
+ // Stateless Mode
284
+ // =========================================================================
285
+
286
+ /**
287
+ * Setup stateless transport (single transport, no session management)
288
+ */
289
+ private async setupStateless(server: McpServer): Promise<void> {
290
+ const statelessTransport = new StreamableHTTPServerTransport({
291
+ sessionIdGenerator: undefined,
292
+ enableJsonResponse: true,
293
+ })
294
+
295
+ await server.connect(statelessTransport)
296
+ logger.info('Stateless transport connected', { module: 'HTTP' })
297
+
298
+ // POST /mcp — all requests go to the same transport
299
+ this.app.post('/mcp', (req: Request, res: Response): void => {
300
+ void statelessTransport.handleRequest(
301
+ req as unknown as IncomingMessage,
302
+ res as unknown as ServerResponse,
303
+ req.body as unknown
304
+ )
305
+ })
306
+
307
+ // GET /mcp — SSE not available in stateless mode
308
+ this.app.get('/mcp', (_req: Request, res: Response): void => {
309
+ res.status(405).json({
310
+ jsonrpc: '2.0',
311
+ error: {
312
+ code: -32000,
313
+ message: 'SSE streaming not available in stateless mode',
314
+ },
315
+ id: null,
316
+ })
317
+ })
318
+
319
+ // DELETE /mcp — no-op in stateless mode
320
+ this.app.delete('/mcp', (_req: Request, res: Response): void => {
321
+ res.status(204).end()
322
+ })
323
+ }
324
+
325
+ // =========================================================================
326
+ // Stateful Mode
327
+ // =========================================================================
328
+
329
+ /** Update the last-activity timestamp for a session */
330
+ private touchSession(sid: string): void {
331
+ this.sessionLastActivity.set(sid, Date.now())
332
+ }
333
+
334
+ /**
335
+ * Setup stateful transport (multi-session, SSE streaming)
336
+ */
337
+ private setupStateful(server: McpServer): void {
338
+ // Start session timeout sweep (runs every 5 minutes)
339
+ this.sessionSweepTimer = setInterval(() => {
340
+ const now = Date.now()
341
+ for (const [sid, lastActivity] of this.sessionLastActivity) {
342
+ const idleMs = now - lastActivity
343
+ if (idleMs <= SESSION_TIMEOUT_MS) continue
344
+
345
+ // Expire idle Streamable HTTP sessions
346
+ if (this.transports.has(sid)) {
347
+ logger.info('Expiring idle HTTP session', {
348
+ module: 'HTTP',
349
+ sessionId: sid,
350
+ idleMinutes: Math.round(idleMs / 60_000),
351
+ })
352
+ const t = this.transports.get(sid)
353
+ if (t) {
354
+ void t.close()
355
+ }
356
+ this.transports.delete(sid)
357
+ this.sessionLastActivity.delete(sid)
358
+ }
359
+
360
+ // Expire idle Legacy SSE sessions
361
+ if (this.sseTransports.has(sid)) {
362
+ logger.info('Expiring idle SSE session', {
363
+ module: 'HTTP',
364
+ sessionId: sid,
365
+ idleMinutes: Math.round(idleMs / 60_000),
366
+ })
367
+ const t = this.sseTransports.get(sid)
368
+ if (t) {
369
+ void t.close()
370
+ }
371
+ this.sseTransports.delete(sid)
372
+ this.sessionLastActivity.delete(sid)
373
+ }
374
+ }
375
+ }, SESSION_SWEEP_INTERVAL_MS)
376
+
377
+ // POST /mcp — Handle JSON-RPC requests
378
+ this.app.post('/mcp', (req: Request, res: Response): void => {
379
+ const sessionId = req.headers['mcp-session-id'] as string | undefined
380
+
381
+ void (async () => {
382
+ try {
383
+ let httpTransport: StreamableHTTPServerTransport | undefined
384
+
385
+ if (sessionId !== undefined && this.transports.has(sessionId)) {
386
+ // Cross-protocol guard: reject SSE session IDs on /mcp
387
+ if (this.sseTransports.has(sessionId)) {
388
+ res.status(400).json({
389
+ jsonrpc: '2.0',
390
+ error: {
391
+ code: -32000,
392
+ message:
393
+ 'Bad Request: Session uses Legacy SSE transport, not Streamable HTTP',
394
+ },
395
+ id: null,
396
+ })
397
+ return
398
+ }
399
+
400
+ // Reuse existing transport and refresh session activity
401
+ this.touchSession(sessionId)
402
+ httpTransport = this.transports.get(sessionId)
403
+ } else if (sessionId === undefined && isInitializeRequest(req.body)) {
404
+ // New initialization request — create transport
405
+ const newTransport = new StreamableHTTPServerTransport({
406
+ sessionIdGenerator: () => randomUUID(),
407
+ onsessioninitialized: (sid: string) => {
408
+ logger.info('HTTP session initialized', {
409
+ module: 'HTTP',
410
+ sessionId: sid,
411
+ })
412
+ this.transports.set(sid, newTransport)
413
+ this.touchSession(sid)
414
+ },
415
+ })
416
+
417
+ // Clean up on transport close
418
+ newTransport.onclose = () => {
419
+ const sid = newTransport.sessionId
420
+ if (sid !== undefined && this.transports.has(sid)) {
421
+ logger.info('HTTP transport closed', {
422
+ module: 'HTTP',
423
+ sessionId: sid,
424
+ })
425
+ this.transports.delete(sid)
426
+ this.sessionLastActivity.delete(sid)
427
+ }
428
+ }
429
+
430
+ // Connect transport to server
431
+ // SDK McpServer only supports one active transport — close first
432
+ try {
433
+ await server.connect(newTransport)
434
+ } catch {
435
+ await server.close()
436
+ await server.connect(newTransport)
437
+ }
438
+ await newTransport.handleRequest(
439
+ req as unknown as IncomingMessage,
440
+ res as unknown as ServerResponse,
441
+ req.body as unknown
442
+ )
443
+ return
444
+ } else {
445
+ // Invalid request — no session ID or not initialization
446
+ res.status(400).json({
447
+ jsonrpc: '2.0',
448
+ error: {
449
+ code: -32000,
450
+ message: 'Bad Request: No valid session ID provided',
451
+ },
452
+ id: null,
453
+ })
454
+ return
455
+ }
456
+
457
+ // Handle request with existing transport
458
+ if (httpTransport !== undefined) {
459
+ await httpTransport.handleRequest(
460
+ req as unknown as IncomingMessage,
461
+ res as unknown as ServerResponse,
462
+ req.body as unknown
463
+ )
464
+ }
465
+ } catch (error) {
466
+ logger.error('Error handling MCP request', {
467
+ module: 'HTTP',
468
+ error: error instanceof Error ? error.message : String(error),
469
+ })
470
+ if (!res.headersSent) {
471
+ res.status(500).json({
472
+ jsonrpc: '2.0',
473
+ error: { code: -32603, message: 'Internal server error' },
474
+ id: null,
475
+ })
476
+ }
477
+ }
478
+ })()
479
+ })
480
+
481
+ // GET /mcp — SSE stream for server-to-client notifications
482
+ this.app.get('/mcp', (req: Request, res: Response): void => {
483
+ const sessionId = req.headers['mcp-session-id'] as string | undefined
484
+
485
+ if (sessionId === undefined || !this.transports.has(sessionId)) {
486
+ res.status(400).send('Invalid or missing session ID')
487
+ return
488
+ }
489
+
490
+ // Refresh session activity on SSE reconnect
491
+ this.touchSession(sessionId)
492
+
493
+ const lastEventId = req.headers['last-event-id']
494
+ if (lastEventId !== undefined) {
495
+ logger.debug('Client reconnecting with Last-Event-ID', {
496
+ module: 'HTTP',
497
+ sessionId,
498
+ lastEventId,
499
+ })
500
+ }
501
+
502
+ const httpTransport = this.transports.get(sessionId)
503
+ if (httpTransport !== undefined) {
504
+ void httpTransport.handleRequest(
505
+ req as unknown as IncomingMessage,
506
+ res as unknown as ServerResponse
507
+ )
508
+ }
509
+ })
510
+
511
+ // DELETE /mcp — Session termination
512
+ this.app.delete('/mcp', (req: Request, res: Response): void => {
513
+ const sessionId = req.headers['mcp-session-id'] as string | undefined
514
+
515
+ if (sessionId === undefined || !this.transports.has(sessionId)) {
516
+ res.status(400).send('Invalid or missing session ID')
517
+ return
518
+ }
519
+
520
+ logger.info('Session termination requested', {
521
+ module: 'HTTP',
522
+ sessionId,
523
+ })
524
+
525
+ const httpTransport = this.transports.get(sessionId)
526
+ if (httpTransport !== undefined) {
527
+ void httpTransport.handleRequest(
528
+ req as unknown as IncomingMessage,
529
+ res as unknown as ServerResponse
530
+ )
531
+ }
532
+ })
533
+ }
534
+
535
+ // =========================================================================
536
+ // Legacy SSE (MCP 2024-11-05)
537
+ // =========================================================================
538
+
539
+ /**
540
+ * Setup Legacy SSE endpoints for backward compatibility.
541
+ * Stateful mode only.
542
+ */
543
+ private setupLegacySSE(server: McpServer): void {
544
+ // GET /sse — Open Legacy SSE connection
545
+ this.app.get('/sse', (req: Request, res: Response): void => {
546
+ logger.info('Legacy SSE connection requested', { module: 'HTTP' })
547
+
548
+ const sseTransport = new SSEServerTransport(
549
+ '/messages',
550
+ res as unknown as ServerResponse
551
+ )
552
+
553
+ // Store transport by session ID after start
554
+ sseTransport.onclose = () => {
555
+ logger.info('Legacy SSE transport closed', {
556
+ module: 'HTTP',
557
+ sessionId: sseTransport.sessionId,
558
+ })
559
+ this.sseTransports.delete(sseTransport.sessionId)
560
+ }
561
+
562
+ void (async () => {
563
+ try {
564
+ // Connect SSE transport to server
565
+ // SDK McpServer only supports one active transport — close first
566
+ try {
567
+ await server.connect(
568
+ sseTransport as unknown as Parameters<typeof server.connect>[0]
569
+ )
570
+ } catch {
571
+ await server.close()
572
+ await server.connect(
573
+ sseTransport as unknown as Parameters<typeof server.connect>[0]
574
+ )
575
+ }
576
+ // Note: server.connect() auto-calls start() on SSEServerTransport
577
+ this.sseTransports.set(sseTransport.sessionId, sseTransport)
578
+ this.touchSession(sseTransport.sessionId)
579
+ logger.info('Legacy SSE connection established', {
580
+ module: 'HTTP',
581
+ sessionId: sseTransport.sessionId,
582
+ })
583
+ } catch (error) {
584
+ logger.error('Error starting SSE transport', {
585
+ module: 'HTTP',
586
+ error: error instanceof Error ? error.message : String(error),
587
+ })
588
+ if (!res.headersSent) {
589
+ res.status(500).end()
590
+ }
591
+ }
592
+ })()
593
+
594
+ // Clean up when client disconnects
595
+ req.on('close', () => {
596
+ this.sseTransports.delete(sseTransport.sessionId)
597
+ this.sessionLastActivity.delete(sseTransport.sessionId)
598
+ })
599
+ })
600
+
601
+ // POST /messages?sessionId=<id> — Route messages to Legacy SSE transport
602
+ this.app.post('/messages', (req: Request, res: Response): void => {
603
+ const sessionId =
604
+ typeof req.query['sessionId'] === 'string' ? req.query['sessionId'] : undefined
605
+
606
+ if (sessionId === undefined) {
607
+ res.status(400).json({
608
+ jsonrpc: '2.0',
609
+ error: { code: -32000, message: 'Missing sessionId parameter' },
610
+ id: null,
611
+ })
612
+ return
613
+ }
614
+
615
+ const transport = this.sseTransports.get(sessionId)
616
+ if (transport === undefined) {
617
+ res.status(404).json({
618
+ jsonrpc: '2.0',
619
+ error: { code: -32000, message: 'Session not found' },
620
+ id: null,
621
+ })
622
+ return
623
+ }
624
+
625
+ // Refresh session activity on message receipt
626
+ this.touchSession(sessionId)
627
+
628
+ void transport.handlePostMessage(
629
+ req as unknown as IncomingMessage,
630
+ res as unknown as ServerResponse,
631
+ req.body as unknown
632
+ )
633
+ })
634
+ }
635
+ }