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.
- package/.dockerignore +131 -122
- package/.gitattributes +29 -0
- package/.github/workflows/docker-publish.yml +1 -1
- package/.github/workflows/lint-and-test.yml +1 -2
- package/.github/workflows/secrets-scanning.yml +0 -1
- package/.github/workflows/security-update.yml +6 -6
- package/.vscode/settings.json +17 -15
- package/CHANGELOG.md +1065 -11
- package/DOCKER_README.md +51 -33
- package/Dockerfile +14 -12
- package/README.md +68 -33
- package/SECURITY.md +225 -220
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -1
- package/dist/constants/ServerInstructions.d.ts +1 -1
- package/dist/constants/ServerInstructions.d.ts.map +1 -1
- package/dist/constants/ServerInstructions.js +70 -26
- package/dist/constants/ServerInstructions.js.map +1 -1
- package/dist/constants/icons.d.ts +2 -0
- package/dist/constants/icons.d.ts.map +1 -1
- package/dist/constants/icons.js +6 -0
- package/dist/constants/icons.js.map +1 -1
- package/dist/database/SqliteAdapter.d.ts +51 -10
- package/dist/database/SqliteAdapter.d.ts.map +1 -1
- package/dist/database/SqliteAdapter.js +143 -43
- package/dist/database/SqliteAdapter.js.map +1 -1
- package/dist/filtering/ToolFilter.d.ts +1 -1
- package/dist/filtering/ToolFilter.d.ts.map +1 -1
- package/dist/filtering/ToolFilter.js +7 -1
- package/dist/filtering/ToolFilter.js.map +1 -1
- package/dist/github/GitHubIntegration.d.ts +74 -2
- package/dist/github/GitHubIntegration.d.ts.map +1 -1
- package/dist/github/GitHubIntegration.js +508 -7
- package/dist/github/GitHubIntegration.js.map +1 -1
- package/dist/handlers/prompts/index.js +1 -0
- package/dist/handlers/prompts/index.js.map +1 -1
- package/dist/handlers/resources/index.d.ts.map +1 -1
- package/dist/handlers/resources/index.js +257 -13
- package/dist/handlers/resources/index.js.map +1 -1
- package/dist/handlers/tools/index.d.ts.map +1 -1
- package/dist/handlers/tools/index.js +595 -8
- package/dist/handlers/tools/index.js.map +1 -1
- package/dist/server/McpServer.d.ts +2 -0
- package/dist/server/McpServer.d.ts.map +1 -1
- package/dist/server/McpServer.js +69 -26
- package/dist/server/McpServer.js.map +1 -1
- package/dist/types/index.d.ts +97 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +8 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/progress-utils.d.ts +18 -3
- package/dist/utils/progress-utils.d.ts.map +1 -1
- package/dist/utils/progress-utils.js.map +1 -1
- package/dist/utils/security-utils.d.ts +91 -0
- package/dist/utils/security-utils.d.ts.map +1 -0
- package/dist/utils/security-utils.js +184 -0
- package/dist/utils/security-utils.js.map +1 -0
- package/dist/vector/VectorSearchManager.d.ts +2 -1
- package/dist/vector/VectorSearchManager.d.ts.map +1 -1
- package/dist/vector/VectorSearchManager.js +100 -34
- package/dist/vector/VectorSearchManager.js.map +1 -1
- package/docker-compose.yml +46 -37
- package/mcp-config-example.json +0 -2
- package/package.json +21 -14
- package/releases/v4.3.1.md +69 -0
- package/releases/v4.4.0.md +120 -0
- package/server.json +3 -3
- package/src/cli.ts +11 -0
- package/src/constants/ServerInstructions.ts +70 -26
- package/src/constants/icons.ts +7 -0
- package/src/database/SqliteAdapter.ts +165 -44
- package/src/filtering/ToolFilter.ts +7 -1
- package/src/github/GitHubIntegration.ts +588 -8
- package/src/handlers/prompts/index.ts +1 -0
- package/src/handlers/resources/index.ts +318 -12
- package/src/handlers/tools/index.ts +686 -13
- package/src/server/McpServer.ts +79 -37
- package/src/types/index.ts +98 -0
- package/src/utils/logger.ts +10 -1
- package/src/utils/progress-utils.ts +17 -6
- package/src/utils/security-utils.ts +205 -0
- package/src/vector/VectorSearchManager.ts +110 -39
- package/tests/constants/icons.test.ts +102 -0
- package/tests/constants/server-instructions.test.ts +549 -0
- package/tests/database/sqlite-adapter.bench.ts +63 -0
- package/tests/database/sqlite-adapter.test.ts +555 -0
- package/tests/filtering/tool-filter.test.ts +266 -0
- package/tests/github/github-integration.test.ts +1024 -0
- package/tests/handlers/github-resource-handlers.test.ts +473 -0
- package/tests/handlers/github-tool-handlers.test.ts +556 -0
- package/tests/handlers/prompt-handlers.test.ts +91 -0
- package/tests/handlers/resource-handlers.test.ts +339 -0
- package/tests/handlers/tool-handlers.test.ts +497 -0
- package/tests/handlers/vector-tool-handlers.test.ts +238 -0
- package/tests/security/sql-injection.test.ts +347 -0
- package/tests/server/mcp-server.bench.ts +55 -0
- package/tests/server/mcp-server.test.ts +675 -0
- package/tests/utils/logger.test.ts +180 -0
- package/tests/utils/mcp-logger.test.ts +212 -0
- package/tests/utils/progress-utils.test.ts +156 -0
- package/tests/utils/security-utils.test.ts +82 -0
- package/tests/vector/vector-search-manager.test.ts +335 -0
- package/tests/vector/vector-search.bench.ts +53 -0
- package/vitest.config.ts +15 -0
- package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
- 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
|
+
})
|