memory-journal-mcp 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.dockerignore +131 -122
  2. package/.gitattributes +29 -0
  3. package/.github/workflows/docker-publish.yml +1 -1
  4. package/.github/workflows/lint-and-test.yml +1 -2
  5. package/.github/workflows/secrets-scanning.yml +0 -1
  6. package/.github/workflows/security-update.yml +6 -6
  7. package/.vscode/settings.json +17 -15
  8. package/CHANGELOG.md +1065 -11
  9. package/DOCKER_README.md +51 -33
  10. package/Dockerfile +14 -12
  11. package/README.md +68 -33
  12. package/SECURITY.md +225 -220
  13. package/dist/cli.js +7 -0
  14. package/dist/cli.js.map +1 -1
  15. package/dist/constants/ServerInstructions.d.ts +1 -1
  16. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  17. package/dist/constants/ServerInstructions.js +70 -26
  18. package/dist/constants/ServerInstructions.js.map +1 -1
  19. package/dist/constants/icons.d.ts +2 -0
  20. package/dist/constants/icons.d.ts.map +1 -1
  21. package/dist/constants/icons.js +6 -0
  22. package/dist/constants/icons.js.map +1 -1
  23. package/dist/database/SqliteAdapter.d.ts +51 -10
  24. package/dist/database/SqliteAdapter.d.ts.map +1 -1
  25. package/dist/database/SqliteAdapter.js +143 -43
  26. package/dist/database/SqliteAdapter.js.map +1 -1
  27. package/dist/filtering/ToolFilter.d.ts +1 -1
  28. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  29. package/dist/filtering/ToolFilter.js +7 -1
  30. package/dist/filtering/ToolFilter.js.map +1 -1
  31. package/dist/github/GitHubIntegration.d.ts +74 -2
  32. package/dist/github/GitHubIntegration.d.ts.map +1 -1
  33. package/dist/github/GitHubIntegration.js +508 -7
  34. package/dist/github/GitHubIntegration.js.map +1 -1
  35. package/dist/handlers/prompts/index.js +1 -0
  36. package/dist/handlers/prompts/index.js.map +1 -1
  37. package/dist/handlers/resources/index.d.ts.map +1 -1
  38. package/dist/handlers/resources/index.js +257 -13
  39. package/dist/handlers/resources/index.js.map +1 -1
  40. package/dist/handlers/tools/index.d.ts.map +1 -1
  41. package/dist/handlers/tools/index.js +595 -8
  42. package/dist/handlers/tools/index.js.map +1 -1
  43. package/dist/server/McpServer.d.ts +2 -0
  44. package/dist/server/McpServer.d.ts.map +1 -1
  45. package/dist/server/McpServer.js +69 -26
  46. package/dist/server/McpServer.js.map +1 -1
  47. package/dist/types/index.d.ts +97 -0
  48. package/dist/types/index.d.ts.map +1 -1
  49. package/dist/types/index.js.map +1 -1
  50. package/dist/utils/logger.d.ts +1 -0
  51. package/dist/utils/logger.d.ts.map +1 -1
  52. package/dist/utils/logger.js +8 -1
  53. package/dist/utils/logger.js.map +1 -1
  54. package/dist/utils/progress-utils.d.ts +18 -3
  55. package/dist/utils/progress-utils.d.ts.map +1 -1
  56. package/dist/utils/progress-utils.js.map +1 -1
  57. package/dist/utils/security-utils.d.ts +91 -0
  58. package/dist/utils/security-utils.d.ts.map +1 -0
  59. package/dist/utils/security-utils.js +184 -0
  60. package/dist/utils/security-utils.js.map +1 -0
  61. package/dist/vector/VectorSearchManager.d.ts +2 -1
  62. package/dist/vector/VectorSearchManager.d.ts.map +1 -1
  63. package/dist/vector/VectorSearchManager.js +100 -34
  64. package/dist/vector/VectorSearchManager.js.map +1 -1
  65. package/docker-compose.yml +46 -37
  66. package/mcp-config-example.json +0 -2
  67. package/package.json +21 -14
  68. package/releases/v4.3.1.md +69 -0
  69. package/releases/v4.4.0.md +120 -0
  70. package/server.json +3 -3
  71. package/src/cli.ts +11 -0
  72. package/src/constants/ServerInstructions.ts +70 -26
  73. package/src/constants/icons.ts +7 -0
  74. package/src/database/SqliteAdapter.ts +165 -44
  75. package/src/filtering/ToolFilter.ts +7 -1
  76. package/src/github/GitHubIntegration.ts +588 -8
  77. package/src/handlers/prompts/index.ts +1 -0
  78. package/src/handlers/resources/index.ts +318 -12
  79. package/src/handlers/tools/index.ts +686 -13
  80. package/src/server/McpServer.ts +79 -37
  81. package/src/types/index.ts +98 -0
  82. package/src/utils/logger.ts +10 -1
  83. package/src/utils/progress-utils.ts +17 -6
  84. package/src/utils/security-utils.ts +205 -0
  85. package/src/vector/VectorSearchManager.ts +110 -39
  86. package/tests/constants/icons.test.ts +102 -0
  87. package/tests/constants/server-instructions.test.ts +549 -0
  88. package/tests/database/sqlite-adapter.bench.ts +63 -0
  89. package/tests/database/sqlite-adapter.test.ts +555 -0
  90. package/tests/filtering/tool-filter.test.ts +266 -0
  91. package/tests/github/github-integration.test.ts +1024 -0
  92. package/tests/handlers/github-resource-handlers.test.ts +473 -0
  93. package/tests/handlers/github-tool-handlers.test.ts +556 -0
  94. package/tests/handlers/prompt-handlers.test.ts +91 -0
  95. package/tests/handlers/resource-handlers.test.ts +339 -0
  96. package/tests/handlers/tool-handlers.test.ts +497 -0
  97. package/tests/handlers/vector-tool-handlers.test.ts +238 -0
  98. package/tests/security/sql-injection.test.ts +347 -0
  99. package/tests/server/mcp-server.bench.ts +55 -0
  100. package/tests/server/mcp-server.test.ts +675 -0
  101. package/tests/utils/logger.test.ts +180 -0
  102. package/tests/utils/mcp-logger.test.ts +212 -0
  103. package/tests/utils/progress-utils.test.ts +156 -0
  104. package/tests/utils/security-utils.test.ts +82 -0
  105. package/tests/vector/vector-search-manager.test.ts +335 -0
  106. package/tests/vector/vector-search.bench.ts +53 -0
  107. package/vitest.config.ts +15 -0
  108. package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
  109. package/.github/workflows/dependabot-auto-merge.yml +0 -42
@@ -0,0 +1,675 @@
1
+ /**
2
+ * McpServer Tests
3
+ *
4
+ * Tests the createServer() function with mocked MCP SDK, database,
5
+ * vector manager, and GitHub integration. Verifies tool/resource/prompt
6
+ * registration and server initialization flows.
7
+ */
8
+
9
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
10
+
11
+ // ============================================================================
12
+ // Hoisted mocks
13
+ // ============================================================================
14
+
15
+ const {
16
+ mockRegisterTool,
17
+ mockRegisterResource,
18
+ mockRegisterPrompt,
19
+ mockConnect,
20
+ mockServer,
21
+ mockDbInitialize,
22
+ mockDbGetRecentEntries,
23
+ mockDbGetStatistics,
24
+ mockDbClose,
25
+ mockDbGetRawDb,
26
+ mockVectorInitialize,
27
+ mockVectorRebuildIndex,
28
+ mockGitHubIsApiAvailable,
29
+ mockCreateEntry,
30
+ mockStdioTransport,
31
+ mockListTags,
32
+ mockHandlers,
33
+ } = vi.hoisted(() => ({
34
+ mockRegisterTool: vi.fn(),
35
+ mockRegisterResource: vi.fn(),
36
+ mockRegisterPrompt: vi.fn(),
37
+ mockConnect: vi.fn().mockResolvedValue(undefined),
38
+ mockServer: { server: {} },
39
+ mockDbInitialize: vi.fn().mockResolvedValue(undefined),
40
+ mockDbGetRecentEntries: vi.fn().mockReturnValue([
41
+ {
42
+ id: 1,
43
+ content: 'Test entry',
44
+ entryType: 'personal_reflection',
45
+ timestamp: new Date().toISOString(),
46
+ isPersonal: true,
47
+ tags: [],
48
+ },
49
+ ]),
50
+ mockDbGetStatistics: vi.fn().mockReturnValue({
51
+ totalEntries: 5,
52
+ entriesByType: {},
53
+ entriesByPeriod: [],
54
+ causalMetrics: { blocked_by: 0, resolved: 0, caused: 0 },
55
+ }),
56
+ mockDbClose: vi.fn(),
57
+ mockDbGetRawDb: vi.fn().mockReturnValue({
58
+ exec: vi.fn().mockReturnValue([]),
59
+ }),
60
+ mockVectorInitialize: vi.fn().mockResolvedValue(undefined),
61
+ mockVectorRebuildIndex: vi.fn().mockResolvedValue(10),
62
+ mockGitHubIsApiAvailable: vi.fn().mockReturnValue(false),
63
+ mockCreateEntry: vi.fn().mockReturnValue({
64
+ id: 1,
65
+ content: 'test',
66
+ entryType: 'personal_reflection',
67
+ timestamp: new Date().toISOString(),
68
+ isPersonal: true,
69
+ tags: [],
70
+ }),
71
+ mockStdioTransport: {},
72
+ mockListTags: vi.fn().mockReturnValue([]),
73
+ mockHandlers: {
74
+ get: {} as Record<string, Function>,
75
+ post: {} as Record<string, Function>,
76
+ delete: {} as Record<string, Function>,
77
+ all: {} as Record<string, Function>,
78
+ use: {} as Record<string, Function>,
79
+ },
80
+ }))
81
+
82
+ // ============================================================================
83
+ // Module mocks
84
+ // ============================================================================
85
+
86
+ vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
87
+ McpServer: function () {
88
+ return {
89
+ registerTool: mockRegisterTool,
90
+ registerResource: mockRegisterResource,
91
+ registerPrompt: mockRegisterPrompt,
92
+ connect: mockConnect,
93
+ server: mockServer,
94
+ }
95
+ },
96
+ ResourceTemplate: function () {
97
+ return {
98
+ uriTemplate: { template: 'mock-template' },
99
+ }
100
+ },
101
+ }))
102
+
103
+ vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
104
+ StdioServerTransport: function () {
105
+ return mockStdioTransport
106
+ },
107
+ }))
108
+
109
+ vi.mock('@modelcontextprotocol/sdk/server/streamableHttp.js', () => ({
110
+ StreamableHTTPServerTransport: function () {
111
+ return {
112
+ handleRequest: vi.fn().mockResolvedValue(undefined),
113
+ close: vi.fn().mockResolvedValue(undefined),
114
+ sessionId: undefined,
115
+ }
116
+ },
117
+ }))
118
+
119
+ vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
120
+ isInitializeRequest: vi.fn().mockReturnValue(false),
121
+ }))
122
+
123
+ vi.mock('../../src/database/SqliteAdapter.js', () => ({
124
+ SqliteAdapter: function () {
125
+ return {
126
+ initialize: mockDbInitialize,
127
+ getRecentEntries: mockDbGetRecentEntries,
128
+ getStatistics: mockDbGetStatistics,
129
+ getActiveEntryCount: vi.fn().mockReturnValue(5),
130
+ createEntry: mockCreateEntry,
131
+ getEntryById: vi.fn().mockReturnValue(null),
132
+ searchEntries: vi.fn().mockReturnValue([]),
133
+ searchByDateRange: vi.fn().mockReturnValue([]),
134
+ getRelationships: vi.fn().mockReturnValue([]),
135
+ linkEntries: vi.fn().mockReturnValue({ id: 1, relationshipType: 'references' }),
136
+ updateEntry: vi.fn().mockReturnValue(null),
137
+ deleteEntry: vi.fn().mockReturnValue(true),
138
+ listTags: mockListTags,
139
+ mergeTags: vi.fn().mockReturnValue({ sourceDeleted: true, entriesUpdated: 0 }),
140
+ getHealthStatus: vi.fn().mockReturnValue({
141
+ database: { path: 'test.db', entryCount: 5, sizeBytes: 1000 },
142
+ }),
143
+ exportToFile: vi.fn().mockReturnValue({
144
+ filename: 'backup.db',
145
+ path: '/tmp/backup.db',
146
+ sizeBytes: 1000,
147
+ }),
148
+ listBackups: vi.fn().mockReturnValue([]),
149
+ getTagsForEntry: vi.fn().mockReturnValue([]),
150
+ getRawDb: mockDbGetRawDb,
151
+ getEntriesPage: vi.fn().mockReturnValue([]),
152
+ close: mockDbClose,
153
+ }
154
+ },
155
+ }))
156
+
157
+ vi.mock('../../src/vector/VectorSearchManager.js', () => ({
158
+ VectorSearchManager: function () {
159
+ return {
160
+ initialize: mockVectorInitialize,
161
+ isInitialized: vi.fn().mockReturnValue(false),
162
+ search: vi.fn().mockResolvedValue([]),
163
+ addEntry: vi.fn().mockResolvedValue(true),
164
+ removeEntry: vi.fn().mockResolvedValue(true),
165
+ rebuildIndex: mockVectorRebuildIndex,
166
+ getStats: vi
167
+ .fn()
168
+ .mockResolvedValue({ itemCount: 0, modelName: 'test', dimensions: 384 }),
169
+ generateEmbedding: vi.fn().mockResolvedValue(new Array(384).fill(0)),
170
+ }
171
+ },
172
+ }))
173
+
174
+ vi.mock('../../src/github/GitHubIntegration.js', () => ({
175
+ GitHubIntegration: function () {
176
+ return {
177
+ isApiAvailable: mockGitHubIsApiAvailable,
178
+ getRepoInfo: vi.fn().mockResolvedValue({ owner: null, repo: null, branch: null }),
179
+ getCachedRepoInfo: vi.fn().mockReturnValue(null),
180
+ getRepoContext: vi.fn().mockResolvedValue(null),
181
+ getIssues: vi.fn().mockResolvedValue([]),
182
+ getIssue: vi.fn().mockResolvedValue(null),
183
+ createIssue: vi.fn().mockResolvedValue(null),
184
+ closeIssue: vi.fn().mockResolvedValue(null),
185
+ getPullRequests: vi.fn().mockResolvedValue([]),
186
+ getPullRequest: vi.fn().mockResolvedValue(null),
187
+ getWorkflowRuns: vi.fn().mockResolvedValue([]),
188
+ getProjectKanban: vi.fn().mockResolvedValue(null),
189
+ getMilestones: vi.fn().mockResolvedValue([]),
190
+ getMilestone: vi.fn().mockResolvedValue(null),
191
+ createMilestone: vi.fn().mockResolvedValue(null),
192
+ updateMilestone: vi.fn().mockResolvedValue(null),
193
+ deleteMilestone: vi.fn().mockResolvedValue(null),
194
+ moveProjectItem: vi.fn().mockResolvedValue({ success: false }),
195
+ addProjectItem: vi.fn().mockResolvedValue({ success: false }),
196
+ clearCache: vi.fn(),
197
+ invalidateCache: vi.fn(),
198
+ }
199
+ },
200
+ }))
201
+
202
+ // Mock express to avoid actual HTTP server creation
203
+ vi.mock('express', () => {
204
+ const mockApp = {
205
+ use: vi.fn().mockImplementation((...args: unknown[]) => {
206
+ if (args.length === 1 && typeof args[0] === 'function') {
207
+ mockHandlers.use['*'] = args[0] as () => void
208
+ }
209
+ }),
210
+ get: vi.fn().mockImplementation((path: string, handler: unknown) => {
211
+ mockHandlers.get[path] = handler as () => void
212
+ }),
213
+ post: vi.fn().mockImplementation((path: string, handler: unknown) => {
214
+ mockHandlers.post[path] = handler as () => void
215
+ }),
216
+ delete: vi.fn().mockImplementation((path: string, handler: unknown) => {
217
+ mockHandlers.delete[path] = handler as () => void
218
+ }),
219
+ all: vi.fn().mockImplementation((path: string, handler: unknown) => {
220
+ mockHandlers.all[path] = handler as () => void
221
+ }),
222
+ listen: vi.fn().mockImplementation((_port: number, _host: string, cb?: () => void) => {
223
+ if (cb) cb()
224
+ return {
225
+ on: vi.fn(),
226
+ close: vi.fn(),
227
+ }
228
+ }),
229
+ }
230
+ const expressFn = vi.fn().mockReturnValue(mockApp)
231
+ return {
232
+ default: Object.assign(expressFn, {
233
+ json: vi.fn().mockReturnValue(vi.fn()),
234
+ }),
235
+ }
236
+ })
237
+
238
+ // Suppress process.on/exit in tests
239
+ vi.spyOn(process, 'on').mockImplementation(() => process)
240
+ vi.spyOn(process, 'exit').mockImplementation((() => {}) as never)
241
+
242
+ // ============================================================================
243
+ // Import after mocks
244
+ // ============================================================================
245
+
246
+ import { createServer, type ServerOptions } from '../../src/server/McpServer.js'
247
+
248
+ describe('McpServer', () => {
249
+ beforeEach(() => {
250
+ vi.clearAllMocks()
251
+ })
252
+
253
+ // ========================================================================
254
+ // Server initialization
255
+ // ========================================================================
256
+
257
+ describe('createServer - stdio transport', () => {
258
+ it('should initialize database and register tools/resources/prompts', async () => {
259
+ await createServer({
260
+ transport: 'stdio',
261
+ dbPath: './test-server.db',
262
+ })
263
+
264
+ // Database should be initialized
265
+ expect(mockDbInitialize).toHaveBeenCalledOnce()
266
+
267
+ // Tools should be registered
268
+ expect(mockRegisterTool).toHaveBeenCalled()
269
+ expect(mockRegisterTool.mock.calls.length).toBeGreaterThan(10)
270
+
271
+ // Resources should be registered
272
+ expect(mockRegisterResource).toHaveBeenCalled()
273
+ expect(mockRegisterResource.mock.calls.length).toBeGreaterThan(5)
274
+
275
+ // Prompts should be registered
276
+ expect(mockRegisterPrompt).toHaveBeenCalled()
277
+
278
+ // Should connect with stdio transport
279
+ expect(mockConnect).toHaveBeenCalled()
280
+ })
281
+
282
+ it('should pass tool options with description', async () => {
283
+ await createServer({
284
+ transport: 'stdio',
285
+ dbPath: './test-server.db',
286
+ })
287
+
288
+ // First registered tool should have description
289
+ const firstCall = mockRegisterTool.mock.calls[0] as unknown[]
290
+ expect(firstCall).toBeDefined()
291
+ expect(typeof firstCall[0]).toBe('string') // tool name
292
+ expect(firstCall[1]).toBeDefined() // tool options with description
293
+ })
294
+
295
+ it('should register create_entry tool', async () => {
296
+ await createServer({
297
+ transport: 'stdio',
298
+ dbPath: './test-server.db',
299
+ })
300
+
301
+ // Check that 'create_entry' was registered
302
+ const toolNames = mockRegisterTool.mock.calls.map(
303
+ (call: unknown[]) => call[0]
304
+ ) as string[]
305
+ expect(toolNames).toContain('create_entry')
306
+ expect(toolNames).toContain('get_entry_by_id')
307
+ expect(toolNames).toContain('search_entries')
308
+ })
309
+ })
310
+
311
+ // ========================================================================
312
+ // Tool filter
313
+ // ========================================================================
314
+
315
+ describe('createServer - with tool filter', () => {
316
+ it('should apply tool filter when provided', async () => {
317
+ await createServer({
318
+ transport: 'stdio',
319
+ dbPath: './test-server.db',
320
+ toolFilter: 'core',
321
+ })
322
+
323
+ // Should still register tools (filtered set)
324
+ expect(mockRegisterTool).toHaveBeenCalled()
325
+ // Filtered set should be smaller than full set
326
+ const filteredCount = mockRegisterTool.mock.calls.length
327
+ expect(filteredCount).toBeGreaterThan(0)
328
+ })
329
+ })
330
+
331
+ // ========================================================================
332
+ // Auto-rebuild vector index
333
+ // ========================================================================
334
+
335
+ describe('createServer - auto rebuild index', () => {
336
+ it('should rebuild vector index when autoRebuildIndex is true', async () => {
337
+ await createServer({
338
+ transport: 'stdio',
339
+ dbPath: './test-server.db',
340
+ autoRebuildIndex: true,
341
+ })
342
+
343
+ expect(mockVectorInitialize).toHaveBeenCalledOnce()
344
+ expect(mockVectorRebuildIndex).toHaveBeenCalled()
345
+ })
346
+
347
+ it('should NOT rebuild index when autoRebuildIndex is not set', async () => {
348
+ await createServer({
349
+ transport: 'stdio',
350
+ dbPath: './test-server.db',
351
+ })
352
+
353
+ expect(mockVectorInitialize).not.toHaveBeenCalled()
354
+ expect(mockVectorRebuildIndex).not.toHaveBeenCalled()
355
+ })
356
+ })
357
+
358
+ // ========================================================================
359
+ // Resource registration
360
+ // ========================================================================
361
+
362
+ describe('createServer - resource registration', () => {
363
+ it('should register both static and template resources', async () => {
364
+ await createServer({
365
+ transport: 'stdio',
366
+ dbPath: './test-server.db',
367
+ })
368
+
369
+ // Should have both static and template resources
370
+ const resourceCalls = mockRegisterResource.mock.calls
371
+ expect(resourceCalls.length).toBeGreaterThan(10)
372
+ })
373
+ })
374
+
375
+ // ========================================================================
376
+ // Prompt registration
377
+ // ========================================================================
378
+
379
+ describe('createServer - prompt registration', () => {
380
+ it('should register prompts with argsSchema', async () => {
381
+ await createServer({
382
+ transport: 'stdio',
383
+ dbPath: './test-server.db',
384
+ })
385
+
386
+ const promptCalls = mockRegisterPrompt.mock.calls
387
+ expect(promptCalls.length).toBeGreaterThan(0)
388
+
389
+ // Each prompt has name, options, handler
390
+ const firstPrompt = promptCalls[0] as unknown[]
391
+ expect(typeof firstPrompt[0]).toBe('string') // prompt name
392
+ expect(firstPrompt[1]).toBeDefined() // options with description
393
+ expect(typeof firstPrompt[2]).toBe('function') // handler
394
+ })
395
+ })
396
+
397
+ // ========================================================================
398
+ // HTTP transport (stateless)
399
+ // ========================================================================
400
+
401
+ describe('createServer - HTTP stateless', () => {
402
+ it('should set up express app for stateless HTTP', async () => {
403
+ await createServer({
404
+ transport: 'http',
405
+ dbPath: './test-server.db',
406
+ statelessHttp: true,
407
+ port: 4000,
408
+ host: '0.0.0.0',
409
+ })
410
+
411
+ // Should connect to transport
412
+ expect(mockConnect).toHaveBeenCalled()
413
+ })
414
+ })
415
+
416
+ // ========================================================================
417
+ // HTTP transport (stateful)
418
+ // ========================================================================
419
+
420
+ describe('createServer - HTTP stateful', () => {
421
+ it('should set up express app for stateful HTTP with session management', async () => {
422
+ await createServer({
423
+ transport: 'http',
424
+ dbPath: './test-server.db',
425
+ statelessHttp: false,
426
+ port: 5000,
427
+ host: '127.0.0.1',
428
+ })
429
+
430
+ // Should NOT call connect for stateful mode (connects per-session)
431
+ // The server.connect is only called once for stateless or stdio
432
+ // For stateful, connect is called per new session initialization
433
+ expect(mockRegisterTool).toHaveBeenCalled()
434
+ })
435
+
436
+ it('should configure CORS origin from options', async () => {
437
+ await createServer({
438
+ transport: 'http',
439
+ dbPath: './test-server.db',
440
+ corsOrigin: 'https://example.com',
441
+ })
442
+
443
+ expect(mockRegisterTool).toHaveBeenCalled()
444
+ })
445
+ })
446
+
447
+ // ========================================================================
448
+ // Default project number
449
+ // ========================================================================
450
+
451
+ describe('createServer - defaultProjectNumber', () => {
452
+ it('should pass defaultProjectNumber through to tools', async () => {
453
+ await createServer({
454
+ transport: 'stdio',
455
+ dbPath: './test-server.db',
456
+ defaultProjectNumber: 42,
457
+ })
458
+
459
+ // Tools should be registered (they receive defaultProjectNumber internally)
460
+ expect(mockRegisterTool).toHaveBeenCalled()
461
+ })
462
+ })
463
+
464
+ // ========================================================================
465
+ // Tool handler callbacks
466
+ // ========================================================================
467
+
468
+ describe('tool handler callbacks', () => {
469
+ it('should invoke tool handler and return structured content', async () => {
470
+ await createServer({
471
+ transport: 'stdio',
472
+ dbPath: './test-server.db',
473
+ })
474
+
475
+ // Get the handler for a tool
476
+ const createEntryCalls = mockRegisterTool.mock.calls.filter(
477
+ (call: unknown[]) => call[0] === 'create_entry'
478
+ ) as unknown[][]
479
+
480
+ expect(createEntryCalls.length).toBe(1)
481
+ const handler = createEntryCalls[0]![2] as (
482
+ args: Record<string, unknown>,
483
+ extra: Record<string, unknown>
484
+ ) => Promise<{ content: { type: string; text: string }[] }>
485
+
486
+ const result = await handler({ content: 'Test from mock' }, { _meta: {} })
487
+
488
+ expect(result.content).toBeDefined()
489
+ expect(result.content[0]!.type).toBe('text')
490
+ })
491
+
492
+ it('should return error content when tool throws', async () => {
493
+ // Make createEntry throw
494
+ mockCreateEntry.mockImplementationOnce(() => {
495
+ throw new Error('Database error')
496
+ })
497
+
498
+ await createServer({
499
+ transport: 'stdio',
500
+ dbPath: './test-server.db',
501
+ })
502
+
503
+ const createEntryCalls = mockRegisterTool.mock.calls.filter(
504
+ (call: unknown[]) => call[0] === 'create_entry'
505
+ ) as unknown[][]
506
+
507
+ const handler = createEntryCalls[0]![2] as (
508
+ args: Record<string, unknown>,
509
+ extra: Record<string, unknown>
510
+ ) => Promise<{
511
+ content: { type: string; text: string }[]
512
+ isError?: boolean
513
+ }>
514
+
515
+ const result = await handler({ content: 'Will fail' }, { _meta: {} })
516
+
517
+ expect(result.isError).toBe(true)
518
+ expect(result.content[0]!.text).toContain('Error')
519
+ })
520
+ })
521
+
522
+ // ========================================================================
523
+ // Prompt handler callbacks
524
+ // ========================================================================
525
+
526
+ describe('prompt handler callbacks', () => {
527
+ it('should invoke prompt handler and return messages', async () => {
528
+ await createServer({
529
+ transport: 'stdio',
530
+ dbPath: './test-server.db',
531
+ })
532
+
533
+ const promptCalls = mockRegisterPrompt.mock.calls
534
+ expect(promptCalls.length).toBeGreaterThan(0)
535
+
536
+ // Get the handler for the first prompt
537
+ const handler = promptCalls[0]![2] as (
538
+ args: Record<string, string>
539
+ ) => Promise<{ messages: unknown[] }>
540
+
541
+ const result = await handler({})
542
+
543
+ expect(result.messages).toBeDefined()
544
+ expect(result.messages.length).toBeGreaterThan(0)
545
+ })
546
+ })
547
+
548
+ // ========================================================================
549
+ // Resource handler callbacks
550
+ // ========================================================================
551
+
552
+ describe('resource handler callbacks', () => {
553
+ it('should invoke resource handler and return contents', async () => {
554
+ await createServer({
555
+ transport: 'stdio',
556
+ dbPath: './test-server.db',
557
+ })
558
+
559
+ // Find a static resource handler (e.g., memory://health is a simple one)
560
+ const healthCalls = mockRegisterResource.mock.calls.filter(
561
+ (call: unknown[]) => call[0] === 'Health Status'
562
+ ) as unknown[][]
563
+
564
+ if (healthCalls.length > 0) {
565
+ const handler = healthCalls[0]![3] as (
566
+ uri: URL
567
+ ) => Promise<{ contents: { uri: string; text: string }[] }>
568
+
569
+ const result = await handler(new URL('memory://health'))
570
+
571
+ expect(result.contents).toBeDefined()
572
+ expect(result.contents.length).toBeGreaterThan(0)
573
+ expect(result.contents[0]!.uri).toBe('memory://health')
574
+ }
575
+ })
576
+ })
577
+
578
+ // ========================================================================
579
+ // HTTP Endpoint handlers
580
+ // ========================================================================
581
+
582
+ describe('HTTP endpoint handlers', () => {
583
+ it('should handle POST /mcp in stateless mode', async () => {
584
+ await createServer({
585
+ transport: 'http',
586
+ dbPath: './test-server.db',
587
+ statelessHttp: true,
588
+ })
589
+
590
+ const postHandler = mockHandlers.post['/mcp']
591
+ expect(postHandler).toBeDefined()
592
+
593
+ const mockReq = { body: { jsonrpc: '2.0', id: 1, method: 'initialize' } }
594
+ const mockRes = { status: vi.fn().mockReturnThis(), json: vi.fn(), end: vi.fn() }
595
+
596
+ // Invoke the handler
597
+ if (postHandler) {
598
+ await postHandler(mockReq, mockRes)
599
+ }
600
+ // StreamableHTTPServerTransport.handleRequest should be called (from our mock)
601
+ })
602
+
603
+ it('should handle GET /mcp and DELETE /mcp in stateless mode', async () => {
604
+ await createServer({
605
+ transport: 'http',
606
+ dbPath: './test-server.db',
607
+ statelessHttp: true,
608
+ })
609
+
610
+ const getHandler = mockHandlers.get['/mcp']
611
+ const deleteHandler = mockHandlers.delete['/mcp']
612
+ expect(getHandler).toBeDefined()
613
+ expect(deleteHandler).toBeDefined()
614
+
615
+ const mockReq = {}
616
+ const mockRes = { status: vi.fn().mockReturnThis(), json: vi.fn(), end: vi.fn() }
617
+
618
+ if (getHandler) await getHandler(mockReq, mockRes)
619
+ expect(mockRes.status).toHaveBeenCalledWith(405)
620
+
621
+ if (deleteHandler) await deleteHandler(mockReq, mockRes)
622
+ expect(mockRes.status).toHaveBeenCalledWith(204)
623
+ })
624
+
625
+ it('should handle stateful mode POST /mcp validation failures', async () => {
626
+ await createServer({
627
+ transport: 'http',
628
+ dbPath: './test-server.db',
629
+ statelessHttp: false,
630
+ })
631
+
632
+ const postHandler = mockHandlers.post['/mcp']
633
+ expect(postHandler).toBeDefined()
634
+
635
+ // Missing session ID and not initialization request
636
+ const mockReq = { headers: {}, body: {} } // isInitializeRequest returns false
637
+ const mockRes = { status: vi.fn().mockReturnThis(), json: vi.fn() }
638
+
639
+ if (postHandler) {
640
+ await postHandler(mockReq, mockRes)
641
+ }
642
+
643
+ expect(mockRes.status).toHaveBeenCalledWith(400)
644
+ expect(mockRes.json).toHaveBeenCalledWith(
645
+ expect.objectContaining({
646
+ error: expect.objectContaining({
647
+ message: expect.stringContaining('No valid session ID'),
648
+ }),
649
+ })
650
+ )
651
+ })
652
+
653
+ it('should handle OPTIONS preflight explicitly', async () => {
654
+ await createServer({
655
+ transport: 'http',
656
+ dbPath: './test-server.db',
657
+ })
658
+
659
+ const allHandler = mockHandlers.all['/mcp']
660
+ expect(allHandler).toBeDefined()
661
+
662
+ const mockReqOptions = { method: 'OPTIONS' }
663
+ const mockResOptions = { status: vi.fn().mockReturnThis(), end: vi.fn() }
664
+ const nextFn = vi.fn()
665
+
666
+ if (allHandler) await allHandler(mockReqOptions, mockResOptions, nextFn)
667
+ expect(mockResOptions.status).toHaveBeenCalledWith(204)
668
+ expect(nextFn).not.toHaveBeenCalled()
669
+
670
+ const mockReqGet = { method: 'GET' }
671
+ if (allHandler) await allHandler(mockReqGet, mockResOptions, nextFn)
672
+ expect(nextFn).toHaveBeenCalled()
673
+ })
674
+ })
675
+ })