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.
- package/.github/workflows/codeql.yml +1 -6
- package/.github/workflows/docker-publish.yml +15 -49
- package/.github/workflows/lint-and-test.yml +1 -1
- package/.github/workflows/secrets-scanning.yml +4 -3
- package/.github/workflows/security-update.yml +3 -3
- package/CHANGELOG.md +213 -0
- package/CONTRIBUTING.md +132 -97
- package/DOCKER_README.md +184 -235
- package/Dockerfile +27 -24
- package/README.md +218 -190
- package/SECURITY.md +27 -35
- package/dist/cli.js +16 -1
- package/dist/cli.js.map +1 -1
- package/dist/constants/ServerInstructions.d.ts +5 -1
- package/dist/constants/ServerInstructions.d.ts.map +1 -1
- package/dist/constants/ServerInstructions.js +133 -73
- package/dist/constants/ServerInstructions.js.map +1 -1
- package/dist/constants/icons.d.ts +2 -2
- package/dist/constants/icons.d.ts.map +1 -1
- package/dist/constants/icons.js +7 -6
- package/dist/constants/icons.js.map +1 -1
- package/dist/database/SqliteAdapter.d.ts +37 -24
- package/dist/database/SqliteAdapter.d.ts.map +1 -1
- package/dist/database/SqliteAdapter.js +319 -157
- package/dist/database/SqliteAdapter.js.map +1 -1
- package/dist/database/schema.d.ts +45 -0
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/database/schema.js +92 -0
- package/dist/database/schema.js.map +1 -0
- package/dist/filtering/ToolFilter.d.ts +1 -1
- package/dist/filtering/ToolFilter.d.ts.map +1 -1
- package/dist/filtering/ToolFilter.js +13 -2
- package/dist/filtering/ToolFilter.js.map +1 -1
- package/dist/github/GitHubIntegration.d.ts.map +1 -1
- package/dist/github/GitHubIntegration.js +1 -3
- package/dist/github/GitHubIntegration.js.map +1 -1
- package/dist/handlers/prompts/github.d.ts +12 -0
- package/dist/handlers/prompts/github.d.ts.map +1 -0
- package/dist/handlers/prompts/github.js +178 -0
- package/dist/handlers/prompts/github.js.map +1 -0
- package/dist/handlers/prompts/index.d.ts +23 -2
- package/dist/handlers/prompts/index.d.ts.map +1 -1
- package/dist/handlers/prompts/index.js +7 -432
- package/dist/handlers/prompts/index.js.map +1 -1
- package/dist/handlers/prompts/workflow.d.ts +12 -0
- package/dist/handlers/prompts/workflow.d.ts.map +1 -0
- package/dist/handlers/prompts/workflow.js +277 -0
- package/dist/handlers/prompts/workflow.js.map +1 -0
- package/dist/handlers/resources/core.d.ts +11 -0
- package/dist/handlers/resources/core.d.ts.map +1 -0
- package/dist/handlers/resources/core.js +433 -0
- package/dist/handlers/resources/core.js.map +1 -0
- package/dist/handlers/resources/github.d.ts +11 -0
- package/dist/handlers/resources/github.d.ts.map +1 -0
- package/dist/handlers/resources/github.js +314 -0
- package/dist/handlers/resources/github.js.map +1 -0
- package/dist/handlers/resources/graph.d.ts +11 -0
- package/dist/handlers/resources/graph.d.ts.map +1 -0
- package/dist/handlers/resources/graph.js +204 -0
- package/dist/handlers/resources/graph.js.map +1 -0
- package/dist/handlers/resources/index.d.ts +5 -20
- package/dist/handlers/resources/index.d.ts.map +1 -1
- package/dist/handlers/resources/index.js +16 -1278
- package/dist/handlers/resources/index.js.map +1 -1
- package/dist/handlers/resources/shared.d.ts +60 -0
- package/dist/handlers/resources/shared.d.ts.map +1 -0
- package/dist/handlers/resources/shared.js +49 -0
- package/dist/handlers/resources/shared.js.map +1 -0
- package/dist/handlers/resources/team.d.ts +13 -0
- package/dist/handlers/resources/team.d.ts.map +1 -0
- package/dist/handlers/resources/team.js +119 -0
- package/dist/handlers/resources/team.js.map +1 -0
- package/dist/handlers/resources/templates.d.ts +13 -0
- package/dist/handlers/resources/templates.d.ts.map +1 -0
- package/dist/handlers/resources/templates.js +310 -0
- package/dist/handlers/resources/templates.js.map +1 -0
- package/dist/handlers/tools/admin.d.ts +8 -0
- package/dist/handlers/tools/admin.d.ts.map +1 -0
- package/dist/handlers/tools/admin.js +270 -0
- package/dist/handlers/tools/admin.js.map +1 -0
- package/dist/handlers/tools/analytics.d.ts +8 -0
- package/dist/handlers/tools/analytics.d.ts.map +1 -0
- package/dist/handlers/tools/analytics.js +256 -0
- package/dist/handlers/tools/analytics.js.map +1 -0
- package/dist/handlers/tools/backup.d.ts +8 -0
- package/dist/handlers/tools/backup.d.ts.map +1 -0
- package/dist/handlers/tools/backup.js +224 -0
- package/dist/handlers/tools/backup.js.map +1 -0
- package/dist/handlers/tools/core.d.ts +9 -0
- package/dist/handlers/tools/core.d.ts.map +1 -0
- package/dist/handlers/tools/core.js +326 -0
- package/dist/handlers/tools/core.js.map +1 -0
- package/dist/handlers/tools/export.d.ts +8 -0
- package/dist/handlers/tools/export.d.ts.map +1 -0
- package/dist/handlers/tools/export.js +89 -0
- package/dist/handlers/tools/export.js.map +1 -0
- package/dist/handlers/tools/github/helpers.d.ts +34 -0
- package/dist/handlers/tools/github/helpers.d.ts.map +1 -0
- package/dist/handlers/tools/github/helpers.js +52 -0
- package/dist/handlers/tools/github/helpers.js.map +1 -0
- package/dist/handlers/tools/github/insights-tools.d.ts +8 -0
- package/dist/handlers/tools/github/insights-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/insights-tools.js +104 -0
- package/dist/handlers/tools/github/insights-tools.js.map +1 -0
- package/dist/handlers/tools/github/issue-tools.d.ts +8 -0
- package/dist/handlers/tools/github/issue-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/issue-tools.js +359 -0
- package/dist/handlers/tools/github/issue-tools.js.map +1 -0
- package/dist/handlers/tools/github/kanban-tools.d.ts +8 -0
- package/dist/handlers/tools/github/kanban-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/kanban-tools.js +108 -0
- package/dist/handlers/tools/github/kanban-tools.js.map +1 -0
- package/dist/handlers/tools/github/milestone-tools.d.ts +9 -0
- package/dist/handlers/tools/github/milestone-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/milestone-tools.js +302 -0
- package/dist/handlers/tools/github/milestone-tools.js.map +1 -0
- package/dist/handlers/tools/github/mutation-tools.d.ts +12 -0
- package/dist/handlers/tools/github/mutation-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/mutation-tools.js +15 -0
- package/dist/handlers/tools/github/mutation-tools.js.map +1 -0
- package/dist/handlers/tools/github/read-tools.d.ts +8 -0
- package/dist/handlers/tools/github/read-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/read-tools.js +260 -0
- package/dist/handlers/tools/github/read-tools.js.map +1 -0
- package/dist/handlers/tools/github/schemas.d.ts +467 -0
- package/dist/handlers/tools/github/schemas.d.ts.map +1 -0
- package/dist/handlers/tools/github/schemas.js +335 -0
- package/dist/handlers/tools/github/schemas.js.map +1 -0
- package/dist/handlers/tools/github.d.ts +14 -0
- package/dist/handlers/tools/github.d.ts.map +1 -0
- package/dist/handlers/tools/github.js +28 -0
- package/dist/handlers/tools/github.js.map +1 -0
- package/dist/handlers/tools/index.d.ts +15 -20
- package/dist/handlers/tools/index.d.ts.map +1 -1
- package/dist/handlers/tools/index.js +117 -2909
- package/dist/handlers/tools/index.js.map +1 -1
- package/dist/handlers/tools/relationships.d.ts +8 -0
- package/dist/handlers/tools/relationships.d.ts.map +1 -0
- package/dist/handlers/tools/relationships.js +308 -0
- package/dist/handlers/tools/relationships.js.map +1 -0
- package/dist/handlers/tools/schemas.d.ts +108 -0
- package/dist/handlers/tools/schemas.d.ts.map +1 -0
- package/dist/handlers/tools/schemas.js +122 -0
- package/dist/handlers/tools/schemas.js.map +1 -0
- package/dist/handlers/tools/search.d.ts +8 -0
- package/dist/handlers/tools/search.d.ts.map +1 -0
- package/dist/handlers/tools/search.js +282 -0
- package/dist/handlers/tools/search.js.map +1 -0
- package/dist/handlers/tools/team.d.ts +11 -0
- package/dist/handlers/tools/team.d.ts.map +1 -0
- package/dist/handlers/tools/team.js +239 -0
- package/dist/handlers/tools/team.js.map +1 -0
- package/dist/server/McpServer.d.ts +4 -0
- package/dist/server/McpServer.d.ts.map +1 -1
- package/dist/server/McpServer.js +48 -297
- package/dist/server/McpServer.js.map +1 -1
- package/dist/server/Scheduler.d.ts +91 -0
- package/dist/server/Scheduler.d.ts.map +1 -0
- package/dist/server/Scheduler.js +201 -0
- package/dist/server/Scheduler.js.map +1 -0
- package/dist/transports/http.d.ts +66 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +519 -0
- package/dist/transports/http.js.map +1 -0
- package/dist/types/entities.d.ts +101 -0
- package/dist/types/entities.d.ts.map +1 -0
- package/dist/types/entities.js +5 -0
- package/dist/types/entities.js.map +1 -0
- package/dist/types/filtering.d.ts +34 -0
- package/dist/types/filtering.d.ts.map +1 -0
- package/dist/types/filtering.js +5 -0
- package/dist/types/filtering.js.map +1 -0
- package/dist/types/github.d.ts +166 -0
- package/dist/types/github.d.ts.map +1 -0
- package/dist/types/github.js +5 -0
- package/dist/types/github.js.map +1 -0
- package/dist/types/index.d.ts +35 -292
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -2
- package/dist/types/index.js.map +1 -1
- package/dist/utils/error-helpers.d.ts +37 -0
- package/dist/utils/error-helpers.d.ts.map +1 -0
- package/dist/utils/error-helpers.js +47 -0
- package/dist/utils/error-helpers.js.map +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +6 -3
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/security-utils.d.ts +0 -21
- package/dist/utils/security-utils.d.ts.map +1 -1
- package/dist/utils/security-utils.js +0 -47
- package/dist/utils/security-utils.js.map +1 -1
- package/dist/vector/VectorSearchManager.d.ts.map +1 -1
- package/dist/vector/VectorSearchManager.js +9 -32
- package/dist/vector/VectorSearchManager.js.map +1 -1
- package/docker-compose.yml +11 -2
- package/hooks/README.md +107 -0
- package/hooks/cursor/hooks.json +10 -0
- package/hooks/cursor/memory-journal.mdc +22 -0
- package/hooks/cursor/session-end.sh +19 -0
- package/hooks/kilo-code/session-end-mode.json +11 -0
- package/hooks/kiro/session-end.md +13 -0
- package/mcp-config-example.json +1 -0
- package/package.json +11 -9
- package/playwright.config.ts +29 -0
- package/releases/v4.5.0.md +116 -0
- package/releases/v5.0.0.md +105 -0
- package/scripts/generate-server-instructions.ts +176 -0
- package/scripts/server-instructions-function-body.ts +77 -0
- package/server.json +3 -3
- package/src/cli.ts +45 -1
- package/src/constants/ServerInstructions.ts +133 -73
- package/src/constants/icons.ts +8 -7
- package/src/constants/server-instructions.md +268 -0
- package/src/database/SqliteAdapter.ts +358 -192
- package/src/database/schema.ts +125 -0
- package/src/filtering/ToolFilter.ts +13 -2
- package/src/github/GitHubIntegration.ts +1 -3
- package/src/handlers/prompts/github.ts +209 -0
- package/src/handlers/prompts/index.ts +10 -499
- package/src/handlers/prompts/workflow.ts +314 -0
- package/src/handlers/resources/core.ts +528 -0
- package/src/handlers/resources/github.ts +358 -0
- package/src/handlers/resources/graph.ts +254 -0
- package/src/handlers/resources/index.ts +23 -1570
- package/src/handlers/resources/shared.ts +103 -0
- package/src/handlers/resources/team.ts +133 -0
- package/src/handlers/resources/templates.ts +374 -0
- package/src/handlers/tools/admin.ts +285 -0
- package/src/handlers/tools/analytics.ts +301 -0
- package/src/handlers/tools/backup.ts +242 -0
- package/src/handlers/tools/core.ts +350 -0
- package/src/handlers/tools/export.ts +115 -0
- package/src/handlers/tools/github/helpers.ts +86 -0
- package/src/handlers/tools/github/insights-tools.ts +119 -0
- package/src/handlers/tools/github/issue-tools.ts +439 -0
- package/src/handlers/tools/github/kanban-tools.ts +134 -0
- package/src/handlers/tools/github/milestone-tools.ts +392 -0
- package/src/handlers/tools/github/mutation-tools.ts +17 -0
- package/src/handlers/tools/github/read-tools.ts +328 -0
- package/src/handlers/tools/github/schemas.ts +369 -0
- package/src/handlers/tools/github.ts +36 -0
- package/src/handlers/tools/index.ts +144 -3325
- package/src/handlers/tools/relationships.ts +358 -0
- package/src/handlers/tools/schemas.ts +132 -0
- package/src/handlers/tools/search.ts +343 -0
- package/src/handlers/tools/team.ts +273 -0
- package/src/server/McpServer.ts +63 -358
- package/src/server/Scheduler.ts +278 -0
- package/src/transports/http.ts +635 -0
- package/src/types/entities.ts +145 -0
- package/src/types/filtering.ts +54 -0
- package/src/types/github.ts +180 -0
- package/src/types/index.ts +67 -375
- package/src/utils/error-helpers.ts +52 -0
- package/src/utils/logger.ts +6 -3
- package/src/utils/security-utils.ts +0 -52
- package/src/vector/VectorSearchManager.ts +9 -33
- package/tests/constants/icons.test.ts +1 -2
- package/tests/constants/server-instructions.test.ts +30 -4
- package/tests/database/sqlite-adapter.test.ts +91 -7
- package/tests/e2e/auth.spec.ts +154 -0
- package/tests/e2e/health.spec.ts +63 -0
- package/tests/e2e/protocols.spec.ts +134 -0
- package/tests/e2e/resources.spec.ts +103 -0
- package/tests/e2e/scheduler.spec.ts +79 -0
- package/tests/e2e/security.spec.ts +91 -0
- package/tests/e2e/sessions.spec.ts +95 -0
- package/tests/e2e/stateless.spec.ts +121 -0
- package/tests/e2e/tools.spec.ts +111 -0
- package/tests/filtering/tool-filter.test.ts +46 -0
- package/tests/handlers/error-path-coverage.test.ts +324 -0
- package/tests/handlers/github-resource-handlers.test.ts +453 -0
- package/tests/handlers/github-tool-handlers.test.ts +899 -0
- package/tests/handlers/prompt-handler-coverage.test.ts +106 -0
- package/tests/handlers/prompt-handlers.test.ts +40 -0
- package/tests/handlers/resource-handler-coverage.test.ts +181 -0
- package/tests/handlers/resource-handlers.test.ts +33 -9
- package/tests/handlers/search-tool-handlers.test.ts +272 -0
- package/tests/handlers/targeted-gap-closure.test.ts +387 -0
- package/tests/handlers/team-resource-handlers.test.ts +156 -0
- package/tests/handlers/team-tool-handlers.test.ts +301 -0
- package/tests/handlers/tool-handler-coverage.test.ts +469 -0
- package/tests/handlers/tool-handlers.test.ts +2 -2
- package/tests/security/sql-injection.test.ts +3 -54
- package/tests/server/mcp-server.test.ts +503 -8
- package/tests/server/scheduler.test.ts +400 -0
- package/tests/transports/http-transport.test.ts +620 -0
- package/tests/vector/vector-search-manager.test.ts +60 -0
- package/vitest.config.ts +4 -1
- package/.memory-journal-team.db +0 -0
- 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
|
-
|
|
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.
|
|
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
|
-
//
|
|
239
|
-
vi.spyOn(process, 'on').mockImplementation(() =>
|
|
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
|
-
|
|
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
|
-
|
|
518
|
-
|
|
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
|
})
|